From e618392e54319d51e7b7cd9bb97395dee6492857 Mon Sep 17 00:00:00 2001 From: dogukanoksuz Date: Thu, 25 Jun 2026 14:03:44 +0300 Subject: [PATCH 01/11] feat: new oidc service for improved login security via openid --- .../Authentication/OIDC/OIDCFlowService.php | 308 +++++ .../Authentication/OIDC/OIDCRoleMapper.php | 133 ++ .../Authentication/OIDC/OIDCTokenStore.php | 52 + .../OIDC/OIDCUserProvisioner.php | 112 ++ .../OIDC/OpenIDConnectClient.php | 217 ++++ .../Authentication/OIDCAuthenticator.php | 712 +---------- app/Providers/OIDCServiceProvider.php | 41 + composer.json | 1 + composer.lock | 1083 ++++++++--------- config/app.php | 1 + storage/VERSION | 2 +- 11 files changed, 1398 insertions(+), 1264 deletions(-) create mode 100644 app/Classes/Authentication/OIDC/OIDCFlowService.php create mode 100644 app/Classes/Authentication/OIDC/OIDCRoleMapper.php create mode 100644 app/Classes/Authentication/OIDC/OIDCTokenStore.php create mode 100644 app/Classes/Authentication/OIDC/OIDCUserProvisioner.php create mode 100644 app/Classes/Authentication/OIDC/OpenIDConnectClient.php create mode 100644 app/Providers/OIDCServiceProvider.php diff --git a/app/Classes/Authentication/OIDC/OIDCFlowService.php b/app/Classes/Authentication/OIDC/OIDCFlowService.php new file mode 100644 index 00000000..be0e1728 --- /dev/null +++ b/app/Classes/Authentication/OIDC/OIDCFlowService.php @@ -0,0 +1,308 @@ +client = $client ?? new OpenIDConnectClient; + $this->userProvisioner = $userProvisioner ?? new OIDCUserProvisioner; + $this->roleMapper = $roleMapper ?? new OIDCRoleMapper; + $this->tokenStore = $tokenStore ?? new OIDCTokenStore; + } + + /** @var OpenIDConnectClient */ + private $client; + + /** @var OIDCUserProvisioner */ + private $userProvisioner; + + /** @var OIDCRoleMapper */ + private $roleMapper; + + /** @var OIDCTokenStore */ + private $tokenStore; + + /** + * OIDC flow'unu başlat - frontend'e redirect URL'i döndür. + */ + public function initiate(Request $request): JsonResponse + { + $state = Str::random(40); + $nonce = Str::random(32); + + $redirectPath = null; + if ($request->has('redirect_path')) { + $redirectPath = $this->validateRedirectPath($request->input('redirect_path')); + } + + Cache::put(self::STATE_CACHE_PREFIX.$state, [ + 'nonce' => $nonce, + 'ip' => $request->ip(), + 'user_agent' => $request->userAgent(), + 'redirect_path' => $redirectPath, + 'created_at' => now()->toDateTimeString(), + ], self::STATE_TTL); + + $authUrl = $this->client->buildAuthorizationUrl($state, $nonce); + + Log::info('OIDC flow initiated', [ + 'state' => $state, + 'ip' => $request->ip(), + 'user_agent' => $request->userAgent(), + ]); + + return response()->json([ + 'message' => 'OIDC provider\'a yönlendiriliyor...', + 'redirect_required' => true, + 'redirect_url' => $authUrl, + ]); + } + + /** + * OIDC callback'ini handle et. + */ + public function handleCallback(Request $request): JsonResponse|RedirectResponse + { + try { + Log::info('OIDC callback received', [ + 'state' => $request->state, + 'has_code' => $request->has('code'), + 'has_error' => $request->has('error'), + 'ip' => $request->ip(), + ]); + + if ($request->has('error')) { + Log::error('OIDC authentication error: '.$request->error.' - '.$request->error_description); + + return $this->error('OIDC authentication failed: '.($request->error_description ?? $request->error), 401); + } + + if (! $request->has('code')) { + Log::error('OIDC callback received without authorization code'); + + return $this->error('Authorization code not received', 400); + } + + if (! $request->has('state')) { + Log::error('OIDC callback received without state parameter'); + + return $this->error('State parameter not received', 400); + } + + $stateData = Cache::get(self::STATE_CACHE_PREFIX.$request->state); + if (! $stateData) { + Log::error('OIDC state not found in cache', [ + 'state' => $request->state, + 'ip' => $request->ip(), + ]); + + return $this->error('Invalid or expired state parameter', 400); + } + + $result = $this->client->completeAuthorizationCodeFlow( + $request->code, + $stateData['nonce'], + ); + $claims = $result['claims']; + $tokenResponse = $result['token_response']; + + // Jumbojett nonce/issuer/aud/exp/nbf'i doğruladı; ek iat + azp + // kontrolleri burada (spec: iat gelecekte olmamalı, azp multi-aud'de + // client_id'ye eşit olmalı). + if (! $this->validateExtraClaims($claims)) { + return $this->error('ID token claim validation failed', 400); + } + + $user = $this->userProvisioner->findOrCreate($claims); + if (! $user) { + Log::error('OIDC user creation/update failed.'); + + return $this->error('User creation failed', 500); + } + + auth('api')->factory()->setTTL($user->session_time); + + $request->merge([ + 'ip' => $stateData['ip'], + 'user_agent' => $stateData['user_agent'], + 'callback_url' => $request->fullUrl(), + ]); + + $permissions = $this->extractPermissions($tokenResponse, $claims); + if (! empty($permissions)) { + $this->roleMapper->assignByPermissions($user, $permissions); + } + + $externalToken = $this->extractExternalToken($claims); + $this->tokenStore->persist($user, $tokenResponse, $externalToken, $permissions); + + Log::info('OIDC authentication successful', [ + 'user_id' => $user->id, + 'email' => $user->email, + ]); + + Cache::forget(self::STATE_CACHE_PREFIX.$request->state); + + $limanTokenResponse = Authenticator::createNewToken( + auth('api')->login($user), + $request, + ); + + return redirect($stateData['redirect_path'] ?? '/') + ->withCookies($limanTokenResponse->headers->getCookies()); + } catch (OpenIDConnectClientException $e) { + Log::error('OIDC authentication failed: '.$e->getMessage(), [ + 'trace' => $e->getTraceAsString(), + ]); + + return $this->error('Authentication failed: '.$e->getMessage(), 400); + } catch (\Exception $e) { + Log::error('OIDC authentication exception: '.$e->getMessage(), [ + 'trace' => $e->getTraceAsString(), + ]); + + return $this->error('Authentication failed', 500); + } + } + + /** + * Jumbojett'in verifyJWTClaims'i dışında kalan ek OIDC claim kontrolleri: + * - iat gelecekte olmamalı (replay/limit koruması) + * - aud birden fazla ise azp, client_id'ye eşit olmalı (OIDC Core §3.1.3.7) + */ + private function validateExtraClaims(object $claims): bool + { + $array = json_decode(json_encode($claims), true) ?: []; + + if (isset($array['iat']) && (int) $array['iat'] > time() + 60) { + Log::error('OIDC ID token issued in the future', [ + 'iat' => $array['iat'], + 'now' => time(), + ]); + + return false; + } + + $aud = $array['aud'] ?? null; + $azp = $array['azp'] ?? null; + $clientId = env('OIDC_CLIENT_ID'); + + if (is_array($aud) && $azp !== null && $azp !== $clientId) { + Log::error('OIDC ID token azp mismatch', [ + 'expected' => $clientId, + 'actual' => $azp, + ]); + + return false; + } + + return true; + } + + /** + * @return array + */ + private function extractPermissions(object $tokenResponse, object $claims): array + { + $tokenArray = json_decode(json_encode($tokenResponse), true) ?: []; + $claimsArray = json_decode(json_encode($claims), true) ?: []; + + return $tokenArray['permissions'] + ?? $claimsArray['permissions'] + ?? []; + } + + private function extractExternalToken(object $claims): ?string + { + $claimsArray = json_decode(json_encode($claims), true) ?: []; + + return $claimsArray['external_token'] ?? null; + } + + private function error(string $message, int $status): JsonResponse + { + return response()->json(['error' => true, 'message' => $message], $status); + } + + /** + * Open redirect'i önlemek için redirect_path'i doğrula. + * Sadece uygulama içi göreli yollara izin verir. + */ + private function validateRedirectPath(?string $path): ?string + { + if (! $path || trim($path) === '') { + return null; + } + + $path = trim($path); + + if (preg_match('#^\w+:|^//#', $path)) { + Log::warning('Rejected redirect_path with protocol', ['path' => $path]); + + return null; + } + + if (str_contains($path, '@')) { + Log::warning('Rejected redirect_path with @ symbol', ['path' => $path]); + + return null; + } + + if (str_contains($path, '\\')) { + Log::warning('Rejected redirect_path with backslashes', ['path' => $path]); + + return null; + } + + if (! str_starts_with($path, '/')) { + Log::warning('Rejected redirect_path not starting with /', ['path' => $path]); + + return null; + } + + $normalized = preg_replace('#/+#', '/', $path); + + if (preg_match('#\.\.|javascript:|data:|vbscript:#i', $normalized)) { + Log::warning('Rejected redirect_path with suspicious pattern', ['path' => $path]); + + return null; + } + + Log::info('Validated redirect_path', ['original' => $path, 'normalized' => $normalized]); + + return $normalized; + } +} diff --git a/app/Classes/Authentication/OIDC/OIDCRoleMapper.php b/app/Classes/Authentication/OIDC/OIDCRoleMapper.php new file mode 100644 index 00000000..03f48c1d --- /dev/null +++ b/app/Classes/Authentication/OIDC/OIDCRoleMapper.php @@ -0,0 +1,133 @@ +removeAutoRoles($user); + + $matchingRoles = Role::whereIn('name', $permissions)->get(); + + if ($matchingRoles->isEmpty()) { + Log::info('No matching roles found for user permissions', [ + 'user_id' => $user->id, + 'permissions' => $permissions, + ]); + + return; + } + + $assignedRoles = []; + foreach ($matchingRoles as $role) { + if ($this->hasManualAssignment($user, $role)) { + Log::info('User already has manual role assignment, skipping auto-assignment', [ + 'user_id' => $user->id, + 'role_id' => $role->id, + 'role_name' => $role->name, + ]); + + continue; + } + + RoleUser::create([ + 'user_id' => $user->id, + 'role_id' => $role->id, + 'auto' => true, + ]); + + $assignedRoles[] = $role->name; + + AuditLog::write( + 'role', + 'users', + [ + 'role_id' => $role->id, + 'role_name' => $role->name, + 'user_id' => $user->id, + 'user_name' => $user->name, + 'source' => 'oidc_permission_mapping', + 'auto' => true, + ], + 'ROLE_USERS', + [], + $user->id, + ); + } + + if (! empty($assignedRoles)) { + Log::info('User assigned to roles via OIDC permission mapping', [ + 'user_id' => $user->id, + 'user_email' => $user->email, + 'assigned_roles' => $assignedRoles, + 'permissions' => $permissions, + 'auto' => true, + ]); + } + } catch (\Exception $e) { + Log::error('Failed to assign user to roles by permissions: '.$e->getMessage(), [ + 'user_id' => $user->id, + 'permissions' => $permissions, + 'trace' => $e->getTraceAsString(), + ]); + } + } + + private function removeAutoRoles(User $user): void + { + $removedAutoRoles = RoleUser::where('user_id', $user->id) + ->where('auto', true) + ->get(); + + if ($removedAutoRoles->isEmpty()) { + return; + } + + $removedRoleNames = []; + foreach ($removedAutoRoles as $roleUser) { + $role = Role::find($roleUser->role_id); + if ($role) { + $removedRoleNames[] = $role->name; + } + } + + RoleUser::where('user_id', $user->id) + ->where('auto', true) + ->delete(); + + Log::info('Removed auto-assigned roles from user', [ + 'user_id' => $user->id, + 'user_email' => $user->email, + 'removed_roles' => $removedRoleNames, + ]); + } + + private function hasManualAssignment(User $user, Role $role): bool + { + return RoleUser::where([ + 'user_id' => $user->id, + 'role_id' => $role->id, + 'auto' => false, + ])->exists(); + } +} diff --git a/app/Classes/Authentication/OIDC/OIDCTokenStore.php b/app/Classes/Authentication/OIDC/OIDCTokenStore.php new file mode 100644 index 00000000..8b47a300 --- /dev/null +++ b/app/Classes/Authentication/OIDC/OIDCTokenStore.php @@ -0,0 +1,52 @@ + $permissions + */ + public function persist(User $user, object $tokenResponse, ?string $externalToken, array $permissions): void + { + if ($externalToken) { + Oauth2Token::updateOrCreate([ + 'user_id' => $user->id, + 'token_type' => 'EXTERNAL_TOKEN', + ], [ + 'user_id' => $user->id, + 'token_type' => 'EXTERNAL_TOKEN', + 'access_token' => $externalToken, + 'refresh_token' => '', + 'expires_in' => 0, + 'refresh_expires_in' => 0, + 'permissions' => $permissions, + ]); + + return; + } + + Oauth2Token::updateOrCreate([ + 'user_id' => $user->id, + 'token_type' => $tokenResponse->token_type ?? 'Bearer', + ], [ + 'user_id' => $user->id, + 'token_type' => $tokenResponse->token_type ?? 'Bearer', + 'access_token' => $tokenResponse->access_token ?? '', + 'refresh_token' => $tokenResponse->refresh_token ?? '', + 'expires_in' => $tokenResponse->expires_in ?? 0, + 'refresh_expires_in' => $tokenResponse->refresh_expires_in ?? 0, + 'permissions' => $permissions, + ]); + } +} diff --git a/app/Classes/Authentication/OIDC/OIDCUserProvisioner.php b/app/Classes/Authentication/OIDC/OIDCUserProvisioner.php new file mode 100644 index 00000000..3dcac10e --- /dev/null +++ b/app/Classes/Authentication/OIDC/OIDCUserProvisioner.php @@ -0,0 +1,112 @@ +normalize($claims); + + try { + $user = User::where('oidc_sub', $userInfo['sub'])->first(); + + if (! $user) { + if (trim($userInfo['email']) === '') { + throw new \Exception('User creation failed, email value is required'); + } + + $user = User::where('email', $userInfo['email'])->first(); + + if ($user) { + $user->update([ + 'oidc_sub' => $userInfo['sub'], + 'auth_type' => 'oidc', + 'name' => $userInfo['display_name'], + ]); + } else { + $user = $this->createUser($userInfo); + } + } else { + $user->update([ + 'name' => $userInfo['display_name'], + 'email' => $userInfo['email'], + 'auth_type' => 'oidc', + ]); + } + + if (! $user->getJWTIdentifier()) { + Log::error('User JWT identifier is null', [ + 'user_id' => $user->id, + 'user_exists' => $user->exists, + ]); + + return null; + } + + return $user; + } catch (\Exception $e) { + Log::error('User creation/update failed: '.$e->getMessage()); + + return null; + } + } + + /** + * @return array{sub: string, email: string, display_name: string, username: string, external_token: ?string} + */ + private function normalize(object $claims): array + { + $array = json_decode(json_encode($claims), true) ?: []; + + $email = strtolower((string) ($array['email'] ?? '')); + $displayName = $array['name'] + ?? $array['preferred_username'] + ?? $array['nickname'] + ?? $array['email'] + ?? ''; + + return [ + 'sub' => (string) ($array['sub'] ?? ''), + 'email' => $email, + 'display_name' => $displayName, + 'username' => $array['preferred_username'] + ?? (str_contains($email, '@') ? explode('@', $email)[0] : $email), + 'external_token' => $array['external_token'] ?? null, + ]; + } + + private function createUser(array $userInfo): User + { + return DB::transaction(function () use ($userInfo): User { + $newUser = User::create([ + 'oidc_sub' => $userInfo['sub'], + 'name' => $userInfo['display_name'], + 'email' => $userInfo['email'], + 'username' => $userInfo['username'], + 'auth_type' => 'oidc', + 'password' => Hash::make(Str::random(32)), + 'forceChange' => false, + ]); + + $newUser->refresh(); + if (! $newUser->id) { + throw new \Exception('User creation failed - ID is null'); + } + + return $newUser; + }); + } +} diff --git a/app/Classes/Authentication/OIDC/OpenIDConnectClient.php b/app/Classes/Authentication/OIDC/OpenIDConnectClient.php new file mode 100644 index 00000000..e77b41f1 --- /dev/null +++ b/app/Classes/Authentication/OIDC/OpenIDConnectClient.php @@ -0,0 +1,217 @@ + JWKS public key + * HS256/384/512 -> client_secret (güvenli routing, alg-confusion yok) + */ +class OpenIDConnectClient extends BaseOpenIDConnectClient +{ + /** @var array PHP session yerine kullanılan in-memory store */ + private array $memorySession = []; + + /** + * OIDC env yapılandırmasından client oluştur. + */ + public function __construct() + { + $issuer = rtrim((string) env('OIDC_ISSUER_URL'), '/'); + + parent::__construct( + $issuer ?: null, + (string) env('OIDC_CLIENT_ID'), + (string) env('OIDC_CLIENT_SECRET'), + $issuer ?: null, + ); + + $redirectUri = env('OIDC_REDIRECT_URI'); + if ($redirectUri) { + $this->setRedirectURL($redirectUri); + } + + $this->addScope(['profile', 'email']); + + $this->configureExplicitEndpoints(); + + // Issuer kontrolünü trailing slash normalize ederek yap. + $this->setIssuerValidator(fn ($iss) => rtrim((string) $iss, '/') === $issuer); + } + + /** + * .well-known discovery zorunlu olmasın diye elle girilmiş endpoint'leri + * provider config'e yaz. Yoksa library auto-discovery'e düşer. + */ + private function configureExplicitEndpoints(): void + { + $params = []; + + $authEndpoint = env('OIDC_AUTH_ENDPOINT'); + if ($authEndpoint) { + $params['authorization_endpoint'] = $this->resolveEndpoint($authEndpoint); + } + + $tokenEndpoint = env('OIDC_TOKEN_ENDPOINT'); + if ($tokenEndpoint) { + $params['token_endpoint'] = $this->resolveEndpoint($tokenEndpoint); + } + + $userinfoEndpoint = env('OIDC_USERINFO_ENDPOINT'); + if ($userinfoEndpoint) { + $params['userinfo_endpoint'] = $this->resolveEndpoint($userinfoEndpoint); + } + + $jwksUri = env('OIDC_JWKS_URI'); + if ($jwksUri) { + $params['jwks_uri'] = $this->resolveEndpoint($jwksUri); + } + + if (! empty($params)) { + $this->providerConfigParam($params); + } + } + + /** + * Göreli (ör. "/oauth/token") veya tam URL'li endpoint değerlerini mutlak + * URL'ye çevir. + */ + private function resolveEndpoint(string $endpoint): string + { + if (preg_match('#^https?://#i', $endpoint)) { + return $endpoint; + } + + return rtrim((string) env('OIDC_ISSUER_URL'), '/').'/'.ltrim($endpoint, '/'); + } + + /** + * Session metodlarını in-memory store'a yönlendir (stateless API). + */ + protected function setSessionKey(string $key, $value): void + { + $this->memorySession[$key] = $value; + } + + protected function getSessionKey(string $key) + { + return array_key_exists($key, $this->memorySession) ? $this->memorySession[$key] : false; + } + + protected function unsetSessionKey(string $key): void + { + unset($this->memorySession[$key]); + } + + protected function startSession(): void + { + // no-op - PHP session kullanılmıyor + } + + protected function commitSession(): void + { + // no-op + } + + /** + * Doğrudan HTTP redirect + exit'i devre dışı bırak. + * Liman akışı URL'yi JSON ile frontend'e döndürür. + * + * @return never + */ + public function redirect(string $url) + { + throw new OpenIDConnectClientException( + 'Direct redirect is disabled in stateless mode; use buildAuthorizationUrl() instead.' + ); + } + + /** + * Authorization endpoint URL'sini, HTTP redirect yapmadan döndür. + * State ve nonce Liman tarafından üretilip Cache'e yazılır; bu yüzden + * library'nin session tabanlı state/nonce üretimine güvenilmez. + */ + public function buildAuthorizationUrl(string $state, string $nonce): string + { + $authEndpoint = $this->getProviderConfigValue('authorization_endpoint'); + + $params = array_merge($this->getAuthParams(), [ + 'response_type' => 'code', + 'redirect_uri' => $this->getRedirectURL(), + 'client_id' => $this->getClientID(), + 'nonce' => $nonce, + 'state' => $state, + 'scope' => implode(' ', array_merge($this->getScopes(), ['openid'])), + ]); + + $responseTypes = $this->getResponseTypes(); + if (! empty($responseTypes)) { + $params['response_type'] = implode(' ', $responseTypes); + } + + return $authEndpoint.(str_contains($authEndpoint, '?') ? '&' : '?') + .http_build_query($params, '', '&'); + } + + /** + * Authorization code'u token'a çevir, ID token'ı JWKS/client_secret ile + * doğrula ve claim'leri döndür. + * + * @return array{claims: object, token_response: object} + * + * @throws OpenIDConnectClientException + */ + public function completeAuthorizationCodeFlow(string $code, string $nonce): array + { + // verifyJWTClaims nonce kontrolü için in-memory store'a yazıyoruz. + $this->setSessionKey('openid_connect_nonce', $nonce); + + $tokenResponse = $this->requestTokens($code); + + if (isset($tokenResponse->error)) { + throw new OpenIDConnectClientException( + $tokenResponse->error_description ?? ('Token exchange failed: '.$tokenResponse->error) + ); + } + + if (! property_exists($tokenResponse, 'id_token')) { + throw new OpenIDConnectClientException('User did not authorize openid scope.'); + } + + $idToken = $tokenResponse->id_token; + $headers = $this->decodeJWT($idToken); + if (isset($headers->enc)) { + $idToken = $this->handleJweResponse($idToken); + } + + $claims = $this->decodeJWT($idToken, 1); + + // İmzayı JWKS (asimetrik) veya client_secret (HS*) ile doğrula. + $this->verifySignatures($idToken); + + // getIdTokenPayload()/getIdTokenHeader() claim kontrolünde kullanılıyor. + $this->setIdToken($idToken); + + if (! $this->verifyJWTClaims($claims, $tokenResponse->access_token ?? null)) { + throw new OpenIDConnectClientException('Unable to verify JWT claims'); + } + + $this->verifiedClaims = $claims; + $this->accessToken = $tokenResponse->access_token ?? null; + + return ['claims' => $claims, 'token_response' => $tokenResponse]; + } +} diff --git a/app/Classes/Authentication/OIDCAuthenticator.php b/app/Classes/Authentication/OIDCAuthenticator.php index 8367ba87..3f6eb5f8 100644 --- a/app/Classes/Authentication/OIDCAuthenticator.php +++ b/app/Classes/Authentication/OIDCAuthenticator.php @@ -2,717 +2,25 @@ namespace App\Classes\Authentication; -use App\Models\AuditLog; -use App\Models\Oauth2Token; -use App\Models\Role; -use App\Models\RoleUser; -use App\Models\User; +use App\Classes\Authentication\OIDC\OIDCFlowService; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; -use Illuminate\Support\Facades\Http; -use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Hash; -use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Str; +use Illuminate\Http\Request; +/** + * AuthenticatorInterface ile mevcut AuthController kablolamasını koruyan ince + * facade. Tüm OIDC mantığı {@see OIDCFlowService} ve collaborator'larına + * taşındı. + */ class OIDCAuthenticator implements AuthenticatorInterface { public function authenticate($credentials, $request): JsonResponse { - return $this->initiateOIDCFlow($request); + return app(OIDCFlowService::class)->initiate($request); } - - /** - * OIDC flow'unu başlat - frontend'e redirect URL'i döndür - */ - private function initiateOIDCFlow($request): JsonResponse - { - $state = Str::random(40); - $nonce = Str::random(32); - - // Validate and sanitize redirect_path to prevent open redirects - $redirectPath = null; - if ($request->has('redirect_path')) { - $redirectPath = self::validateRedirectPath($request->input('redirect_path')); - } - - // State ve nonce'u cache'te sakla (30 dakika TTL) - Cache::put("oidc_state:{$state}", [ - 'nonce' => $nonce, - 'ip' => $request->ip(), - 'user_agent' => $request->userAgent(), - 'redirect_path' => $redirectPath, - 'created_at' => now()->toDateTimeString() - ], 30 * 60); // 30 dakika - - $params = http_build_query([ - 'client_id' => env('OIDC_CLIENT_ID'), - 'redirect_uri' => env('OIDC_REDIRECT_URI'), - 'response_type' => 'code', - 'scope' => 'openid profile email', - 'state' => $state, - 'nonce' => $nonce, - ]); - - $authEndpoint = env('OIDC_AUTH_ENDPOINT', '/authorize'); - $authUrl = env('OIDC_ISSUER_URL') . $authEndpoint . '?' . $params; - - Log::info("OIDC flow initiated", [ - 'state' => $state, - 'ip' => $request->ip(), - 'user_agent' => $request->userAgent() - ]); - - return response()->json([ - 'message' => 'OIDC provider\'a yönlendiriliyor...', - 'redirect_required' => true, - 'redirect_url' => $authUrl - ]); - } - - /** - * OIDC callback'ini handle et - */ - public static function handleCallback($request): JsonResponse|RedirectResponse - { - try { - Log::info("OIDC callback received", [ - 'state' => $request->state, - 'has_code' => $request->has('code'), - 'has_error' => $request->has('error'), - 'ip' => $request->ip() - ]); - - // Error check - if ($request->has('error')) { - Log::error('OIDC authentication error: ' . $request->error . ' - ' . $request->error_description); - return response()->json([ - 'error' => true, - 'message' => 'OIDC authentication failed: ' . ($request->error_description ?? $request->error) - ], 401); - } - - // Authorization code kontrolü - if (!$request->has('code')) { - Log::error('OIDC callback received without authorization code'); - return response()->json([ - 'error' => true, - 'message' => 'Authorization code not received' - ], 400); - } - - // State parametresi var mı? - if (!$request->has('state')) { - Log::error('OIDC callback received without state parameter'); - return response()->json([ - 'error' => true, - 'message' => 'State parameter not received' - ], 400); - } - - // Cache'ten state bilgilerini al - $stateData = Cache::get("oidc_state:{$request->state}"); - - if (!$stateData) { - Log::error('OIDC state not found in cache', [ - 'state' => $request->state, - 'ip' => $request->ip() - ]); - return response()->json([ - 'error' => true, - 'message' => 'Invalid or expired state parameter' - ], 400); - } - - Log::info("OIDC state found in cache", [ - 'state' => $request->state, - 'cached_data' => $stateData - ]); - - // Authorization code ile token exchange - $tokenEndpoint = env('OIDC_TOKEN_ENDPOINT', '/oauth/token'); - $tokenResponse = Http::asForm()->post(env('OIDC_ISSUER_URL') . $tokenEndpoint, [ - 'grant_type' => 'authorization_code', - 'client_id' => env('OIDC_CLIENT_ID'), - 'client_secret' => env('OIDC_CLIENT_SECRET'), - 'redirect_uri' => env('OIDC_REDIRECT_URI'), - 'code' => $request->code, - ]); - - if (!$tokenResponse->successful()) { - Log::error('OIDC token exchange failed: ' . $tokenResponse->body()); - return response()->json([ - 'error' => true, - 'message' => 'Token exchange failed' - ], 400); - } - - $tokenData = $tokenResponse->json(); - - // ID token'ı decode et ve verify et - $userInfo = self::decodeAndVerifyIdToken($tokenData['id_token']); - - if (!$userInfo) { - Log::error('OIDC ID token verification failed.'); - return response()->json([ - 'error' => true, - 'message' => 'ID token verification failed' - ], 400); - } - - // Nonce kontrolü - if (isset($userInfo['nonce']) && $userInfo['nonce'] !== $stateData['nonce']) { - Log::error('OIDC authentication failed. Invalid nonce.', [ - 'token_nonce' => $userInfo['nonce'], - 'cached_nonce' => $stateData['nonce'] - ]); - return response()->json([ - 'error' => true, - 'message' => 'Invalid nonce' - ], 400); - } - - // User'ı bul veya oluştur - $user = self::findOrCreateUser($userInfo); - - if (!$user) { - Log::error('OIDC user creation/update failed.'); - return response()->json([ - 'error' => true, - 'message' => 'User creation failed' - ], 500); - } - - // User'ın session time'ını set et - auth('api')->factory()->setTTL($user->session_time); - - // Request objesi için orijinal IP ve User-Agent bilgilerini kullan - $request->merge([ - 'ip' => $stateData['ip'], - 'user_agent' => $stateData['user_agent'], - 'callback_url' => $request->fullUrl() // Callback URL'yi ekle - ]); - - - $permissions = []; - if (isset($tokenData['permissions'])) { - $permissions = $tokenData['permissions']; - } - - // Assign user to roles based on matching permission names - if (!empty($permissions)) { - self::assignUserToRolesByPermissions($user, $permissions); - } - - if (isset($userInfo['external_token'])) { - Oauth2Token::updateOrCreate([ - 'user_id' => $user->id, - 'token_type' => "EXTERNAL_TOKEN", - ], [ - 'user_id' => $user->id, - 'token_type' => "EXTERNAL_TOKEN", - 'access_token' => $userInfo['external_token'], - 'refresh_token' => "", - 'expires_in' => 0, - 'refresh_expires_in' => 0, - 'permissions' => $permissions, - ]); - } else { - Oauth2Token::updateOrCreate([ - 'user_id' => $user->id, - 'token_type' => $tokenData['token_type'], - ], [ - 'user_id' => $user->id, - 'token_type' => $tokenData['token_type'], - 'access_token' => $tokenData['access_token'], - 'refresh_token' => $tokenData['refresh_token'] ?? '', - 'expires_in' => $tokenData['expires_in'] ?? 0, - 'refresh_expires_in' => $tokenData['refresh_expires_in'] ?? 0, - 'permissions' => $permissions, - ]); - } - - Log::info("OIDC authentication successful", [ - 'user_id' => $user->id, - 'email' => $user->email - ]); - - // Cache'ten state'i sil - Cache::forget("oidc_state:{$request->state}"); - - // JWT token oluştur ve cookie'ye set et - $tokenResponse = Authenticator::createNewToken( - auth('api')->login($user), - $request - ); - - // Get the redirect path or default to homepage - $redirectPath = $stateData['redirect_path'] ?? '/'; - - // Redirect the user with cookies attached - return redirect($redirectPath) - ->withCookies($tokenResponse->headers->getCookies()); - } catch (\Exception $e) { - Log::error('OIDC authentication exception: ' . $e->getMessage(), [ - 'trace' => $e->getTraceAsString() - ]); - return response()->json([ - 'error' => true, - 'message' => 'Authentication failed' - ], 500); - } - } - - /** - * ID token'ı decode et ve kriptografik olarak verify et - */ - private static function decodeAndVerifyIdToken($idToken): ?array - { - try { - // JWT token'ı parçala - $parts = explode('.', $idToken); - if (count($parts) !== 3) { - Log::error('Invalid JWT token format'); - return null; - } - - // Header ve payload'ı decode et - $header = json_decode(base64_decode(str_pad(strtr($parts[0], '-_', '+/'), strlen($parts[0]) % 4, '=', STR_PAD_RIGHT)), true); - $payload = json_decode(base64_decode(str_pad(strtr($parts[1], '-_', '+/'), strlen($parts[1]) % 4, '=', STR_PAD_RIGHT)), true); - - if (! $header || ! isset($header['alg'])) { - Log::error('OIDC ID token missing header algorithm'); - return null; - } - - // Sadece asimetrik RS256 algoritmasını kabul et - // HS256 gibi simetrik algoritmalar reddedilmeli (client secret ile imzalanmış gibi görünen sahte tokenlar) - if ($header['alg'] !== 'RS256') { - Log::error('OIDC ID token unsupported algorithm', ['alg' => $header['alg']]); - return null; - } - - // JWKS URI'yi belirle ve public key set'ini çek - $jwksUri = self::getJwksUri(); - if (! $jwksUri) { - Log::error('OIDC JWKS URI could not be determined'); - return null; - } - - $jwks = Cache::remember( - 'oidc_jwks:'.md5($jwksUri), - 3600, - function () use ($jwksUri) { - $response = Http::get($jwksUri); - if (! $response->successful()) { - throw new \Exception('Failed to fetch JWKS: '.$response->body()); - } - - return $response->json(); - } - ); - - // Header'daki kid ile eşleşen public key'i JWKS içinde bul - $kid = $header['kid'] ?? null; - $matchedKey = null; - foreach ($jwks['keys'] ?? [] as $jwk) { - if ($kid === null || (isset($jwk['kid']) && $jwk['kid'] === $kid)) { - if (isset($jwk['n'], $jwk['e'])) { - $matchedKey = $jwk; - break; - } - } - } - - if (! $matchedKey) { - Log::error('OIDC ID token signing key not found in JWKS', ['kid' => $kid]); - return null; - } - - // JWK'dan PEM formatında public key oluştur - $publicKey = self::createPemFromModulusAndExponent($matchedKey['n'], $matchedKey['e']); - - // JWT imzasını kriptografik olarak doğrula - $signature = base64_decode(str_pad(strtr($parts[2], '-_', '+/'), strlen($parts[2]) % 4, '=', STR_PAD_RIGHT)); - $signedData = $parts[0].'.'.$parts[1]; - $verifyResult = openssl_verify($signedData, $signature, $publicKey, OPENSSL_ALGO_SHA256); - - if ($verifyResult !== 1) { - Log::error('OIDC ID token signature verification failed', ['openssl_error' => openssl_error_string()]); - return null; - } - - Log::info('JWT signature verified successfully', [ - 'kid' => $kid, - 'sub' => $payload['sub'] ?? null, - ]); - - // Basic validation - if (! $payload || ! isset($payload['sub']) || ! isset($payload['email'])) { - Log::error('OIDC ID token missing required claims', [ - 'has_sub' => isset($payload['sub']), - 'has_email' => isset($payload['email']), - 'payload' => $payload, - ]); - return null; - } - - // Issuer kontrolü (trailing slash normalize edilerek) - $expectedIssuer = rtrim(env('OIDC_ISSUER_URL'), '/'); - $actualIssuer = rtrim($payload['iss'], '/'); - - if ($actualIssuer !== $expectedIssuer) { - Log::error('OIDC ID token issuer mismatch', [ - 'expected' => $expectedIssuer, - 'actual' => $actualIssuer, - ]); - return null; - } - - // Audience kontrolü - if ($payload['aud'] !== env('OIDC_CLIENT_ID')) { - Log::error('OIDC ID token audience mismatch', [ - 'expected' => env('OIDC_CLIENT_ID'), - 'actual' => $payload['aud'], - ]); - return null; - } - - // Expiration kontrolü - if ($payload['exp'] < time()) { - Log::error('OIDC ID token expired', [ - 'exp' => $payload['exp'], - 'now' => time(), - ]); - return null; - } - - // Issued-at kontrolü (gelecekte oluşturulmuş token'ları reddet) - if (isset($payload['iat']) && $payload['iat'] > time() + 60) { - Log::error('OIDC ID token issued in the future', [ - 'iat' => $payload['iat'], - 'now' => time(), - ]); - return null; - } - - return $payload; - } catch (\Exception $e) { - Log::error('ID token decode/verify failed: '.$e->getMessage()); - return null; - } - } - - /** - * OIDC Provider'ın JWKS endpoint URI'sini döndür - */ - private static function getJwksUri(): ?string - { - // Doğrudan yapılandırılmış değer varsa kullan - $configuredUri = env('OIDC_JWKS_URI'); - if ($configuredUri) { - return $configuredUri; - } - - // Auto-discovery - $issuer = rtrim(env('OIDC_ISSUER_URL'), '/'); - if (! $issuer) { - return null; - } - - try { - $response = Http::get($issuer.'/.well-known/openid-configuration'); - if (! $response->successful()) { - Log::error('OIDC discovery failed: '.$response->body()); - return null; - } - - return $response->json()['jwks_uri'] ?? null; - } catch (\Exception $e) { - Log::error('OIDC discovery exception: '.$e->getMessage()); - return null; - } - } - - /** - * JWK (n, e) değerlerinden PEM formatında RSA public key oluştur - */ - private static function createPemFromModulusAndExponent(string $n, string $e): string - { - $modulus = base64_decode(strtr($n, '-_', '+/')); - $publicExponent = base64_decode(strtr($e, '-_', '+/')); - - $modulus = pack('Ca*a*', 2, self::encodeLength(strlen($modulus)), $modulus); - $publicExponent = pack('Ca*a*', 2, self::encodeLength(strlen($publicExponent)), $publicExponent); - - $rsaPublicKey = pack( - 'Ca*a*a*', - 48, - self::encodeLength(strlen($modulus) + strlen($publicExponent)), - $modulus, - $publicExponent - ); - - $rsaPublicKey = pack('Ca*a*', 48, self::encodeLength(strlen($rsaPublicKey)), $rsaPublicKey); - - $encoded = base64_encode($rsaPublicKey); - $pem = "-----BEGIN PUBLIC KEY-----\n"; - $pem .= chunk_split($encoded, 64, "\n"); - $pem .= "-----END PUBLIC KEY-----\n"; - - return $pem; - } - - /** - * DER/ASN.1 length encoding helper - */ - private static function encodeLength(int $length): string - { - if ($length <= 127) { - return chr($length); - } - $temp = ltrim(pack('N', $length), chr(0)); - - return chr(0x80 | strlen($temp)).$temp; - } - /** - * User'ı bul veya oluştur - */ - private static function findOrCreateUser($userInfo): ?User - { - try { - // OIDC sub ID'si ile user'ı ara - $user = User::where('oidc_sub', $userInfo['sub'])->first(); - - if (!$user) { - if (trim($userInfo['email']) === '') { - throw new \Exception('User creation failed, email value is required'); - } - // Email ile user'ı ara - $user = User::where('email', strtolower($userInfo['email']))->first(); - - if ($user) { - // Mevcut user'a OIDC sub'ı ekle - $user->update([ - 'oidc_sub' => $userInfo['sub'], - 'auth_type' => 'oidc', - 'name' => $userInfo['name'] ?? $userInfo['preferred_username'] ?? $userInfo['nickname'] ?? $userInfo['email'], - ]); - } else { - // Database transaction içinde yeni user oluştur - $user = DB::transaction(function () use ($userInfo) { - $newUser = User::create([ - 'oidc_sub' => $userInfo['sub'], - 'name' => $userInfo['name'] ?? $userInfo['preferred_username'] ?? $userInfo['nickname'] ?? $userInfo['email'], - 'email' => strtolower($userInfo['email']), - 'username' => $userInfo['preferred_username'] ?? explode('@', $userInfo['email'])[0], - 'auth_type' => 'oidc', - 'password' => Hash::make(Str::random(32)), // Random password - 'forceChange' => false, - ]); - - // User'ın gerçekten kaydedildiğini ve ID'sinin olduğunu kontrol et - $newUser->refresh(); - if (!$newUser->id) { - throw new \Exception('User creation failed - ID is null'); - } - - return $newUser; - }); - } - } else { - // Mevcut OIDC user'ı güncelle - $user->update([ - 'name' => $userInfo['name'] ?? $userInfo['preferred_username'] ?? $userInfo['nickname'] ?? $userInfo['email'], - 'email' => strtolower($userInfo['email']), - 'auth_type' => 'oidc', - ]); - } - - // Son kontrol: User'ın JWT identifier'ının olduğunu garanti et - if (!$user->getJWTIdentifier()) { - Log::error('User JWT identifier is null', [ - 'user_id' => $user->id, - 'user_exists' => $user->exists, - ]); - return null; - } - - return $user; - - } catch (\Exception $e) { - Log::error('User creation/update failed: ' . $e->getMessage()); - return null; - } - } - - /** - * Validate redirect path to prevent open redirect vulnerabilities - * Only allows relative paths within the application - * - * @param string $path - * @return string|null Sanitized path or null if invalid - */ - private static function validateRedirectPath(?string $path): ?string + public static function handleCallback(Request $request): JsonResponse|RedirectResponse { - if (!$path || trim($path) === '') { - return null; - } - - $path = trim($path); - - // Reject if it contains protocol (http://, https://, ftp://, //) - if (preg_match('#^\w+:|^//#', $path)) { - Log::warning('Rejected redirect_path with protocol', ['path' => $path]); - return null; - } - - // Reject if it contains @ (username in URL) - if (strpos($path, '@') !== false) { - Log::warning('Rejected redirect_path with @ symbol', ['path' => $path]); - return null; - } - - // Reject if it contains backslashes (Windows path or obfuscation attempt) - if (strpos($path, '\\') !== false) { - Log::warning('Rejected redirect_path with backslashes', ['path' => $path]); - return null; - } - - // Only allow paths starting with / - if (!str_starts_with($path, '/')) { - Log::warning('Rejected redirect_path not starting with /', ['path' => $path]); - return null; - } - - // Additional security: reject if path tries to escape with multiple slashes - $normalized = preg_replace('#/+#', '/', $path); - - // Reject any suspicious patterns - if (preg_match('#\.\.|javascript:|data:|vbscript:#i', $normalized)) { - Log::warning('Rejected redirect_path with suspicious pattern', ['path' => $path]); - return null; - } - - Log::info('Validated redirect_path', ['original' => $path, 'normalized' => $normalized]); - - return $normalized; - } - - /** - * Assign user to roles based on matching permission names - * - * This method first removes all auto-assigned roles (auto=true) from the user, - * then assigns new roles from OIDC with auto=true flag. - * This ensures that when a role is removed from OIDC, it gets removed from the user. - * - * @param User $user - * @param array $permissions - * @return void - */ - private static function assignUserToRolesByPermissions(User $user, array $permissions): void - { - try { - // First, remove all auto-assigned roles for this user - $removedAutoRoles = RoleUser::where('user_id', $user->id) - ->where('auto', true) - ->get(); - - if ($removedAutoRoles->isNotEmpty()) { - $removedRoleNames = []; - foreach ($removedAutoRoles as $roleUser) { - $role = Role::find($roleUser->role_id); - if ($role) { - $removedRoleNames[] = $role->name; - } - } - - RoleUser::where('user_id', $user->id) - ->where('auto', true) - ->delete(); - - Log::info('Removed auto-assigned roles from user', [ - 'user_id' => $user->id, - 'user_email' => $user->email, - 'removed_roles' => $removedRoleNames - ]); - } - - // Get all roles that match permission names - $matchingRoles = Role::whereIn('name', $permissions)->get(); - - if ($matchingRoles->isEmpty()) { - Log::info('No matching roles found for user permissions', [ - 'user_id' => $user->id, - 'permissions' => $permissions - ]); - return; - } - - $assignedRoles = []; - foreach ($matchingRoles as $role) { - // Check if user is already manually assigned to this role (auto=false) - $existingManualAssignment = RoleUser::where([ - 'user_id' => $user->id, - 'role_id' => $role->id, - 'auto' => false, - ])->first(); - - if ($existingManualAssignment) { - // User already has manual assignment, skip auto-assignment - Log::info('User already has manual role assignment, skipping auto-assignment', [ - 'user_id' => $user->id, - 'role_id' => $role->id, - 'role_name' => $role->name - ]); - continue; - } - - // Assign user to role with auto=true - RoleUser::create([ - 'user_id' => $user->id, - 'role_id' => $role->id, - 'auto' => true, - ]); - - $assignedRoles[] = $role->name; - - // Audit log - AuditLog::write( - 'role', - 'users', - [ - 'role_id' => $role->id, - 'role_name' => $role->name, - 'user_id' => $user->id, - 'user_name' => $user->name, - 'source' => 'oidc_permission_mapping', - 'auto' => true - ], - "ROLE_USERS", - [], - $user->id, - ); - } - - if (!empty($assignedRoles)) { - Log::info('User assigned to roles via OIDC permission mapping', [ - 'user_id' => $user->id, - 'user_email' => $user->email, - 'assigned_roles' => $assignedRoles, - 'permissions' => $permissions, - 'auto' => true - ]); - } - - } catch (\Exception $e) { - Log::error('Failed to assign user to roles by permissions: ' . $e->getMessage(), [ - 'user_id' => $user->id, - 'permissions' => $permissions, - 'trace' => $e->getTraceAsString() - ]); - } + return app(OIDCFlowService::class)->handleCallback($request); } } diff --git a/app/Providers/OIDCServiceProvider.php b/app/Providers/OIDCServiceProvider.php new file mode 100644 index 00000000..6ee57ea6 --- /dev/null +++ b/app/Providers/OIDCServiceProvider.php @@ -0,0 +1,41 @@ +app->singleton(OpenIDConnectClient::class); + + $this->app->singleton(OIDCUserProvisioner::class); + $this->app->singleton(OIDCRoleMapper::class); + $this->app->singleton(OIDCTokenStore::class); + + $this->app->singleton(OIDCFlowService::class, function ($app) { + return new OIDCFlowService( + $app->make(OpenIDConnectClient::class), + $app->make(OIDCUserProvisioner::class), + $app->make(OIDCRoleMapper::class), + $app->make(OIDCTokenStore::class), + ); + }); + } +} diff --git a/composer.json b/composer.json index 64f3a0d9..13d8c03a 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "bacon/bacon-qr-code": "^3.0", "doctrine/dbal": "^4.2", "guzzlehttp/guzzle": "^7.9", + "jumbojett/openid-connect-php": "^1.0", "laravel/framework": "^12.0", "laravel/helpers": "^1.7", "laravel/reverb": "^1.0", diff --git a/composer.lock b/composer.lock index eaef5139..c3748be4 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": "492893f07bf3f395e268325b09a2bf55", + "content-hash": "f3d1e864eb7812f0c20692ac85421a8a", "packages": [ { "name": "acsystems/keycloak-php-sdk", @@ -133,16 +133,16 @@ }, { "name": "bacon/bacon-qr-code", - "version": "v3.0.3", + "version": "v3.1.1", "source": { "type": "git", "url": "https://github.com/Bacon/BaconQrCode.git", - "reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563" + "reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/36a1cb2b81493fa5b82e50bf8068bf84d1542563", - "reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2", + "reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2", "shasum": "" }, "require": { @@ -182,9 +182,9 @@ "homepage": "https://github.com/Bacon/BaconQrCode", "support": { "issues": "https://github.com/Bacon/BaconQrCode/issues", - "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.3" + "source": "https://github.com/Bacon/BaconQrCode/tree/v3.1.1" }, - "time": "2025-11-19T17:15:36+00:00" + "time": "2026-04-05T21:06:35+00:00" }, { "name": "brick/math", @@ -572,16 +572,16 @@ }, { "name": "doctrine/dbal", - "version": "4.4.2", + "version": "4.4.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "476f7f0fa6ea4aa5364926db7fabdf6049075722" + "reference": "61e730f1658814821a85f2402c945f3883407dec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/476f7f0fa6ea4aa5364926db7fabdf6049075722", - "reference": "476f7f0fa6ea4aa5364926db7fabdf6049075722", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/61e730f1658814821a85f2402c945f3883407dec", + "reference": "61e730f1658814821a85f2402c945f3883407dec", "shasum": "" }, "require": { @@ -658,7 +658,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/4.4.2" + "source": "https://github.com/doctrine/dbal/tree/4.4.3" }, "funding": [ { @@ -674,7 +674,7 @@ "type": "tidelift" } ], - "time": "2026-02-26T12:12:19+00:00" + "time": "2026-03-20T08:52:12+00:00" }, { "name": "doctrine/deprecations", @@ -1074,12 +1074,12 @@ "version": "v6.11.1", "source": { "type": "git", - "url": "https://github.com/firebase/php-jwt.git", + "url": "https://github.com/googleapis/php-jwt.git", "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", "shasum": "" }, @@ -1127,8 +1127,8 @@ "php" ], "support": { - "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.11.1" + "issues": "https://github.com/googleapis/php-jwt/issues", + "source": "https://github.com/googleapis/php-jwt/tree/v6.11.1" }, "time": "2025-04-09T20:32:01+00:00" }, @@ -1267,25 +1267,26 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.10.0", + "version": "7.12.3", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + "reference": "9aa17bcdd777ee31df9fc83c337ca4ca2340def3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", - "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9aa17bcdd777ee31df9fc83c337ca4ca2340def3", + "reference": "9aa17bcdd777ee31df9fc83c337ca4ca2340def3", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.3", - "guzzlehttp/psr7": "^2.8", + "guzzlehttp/promises": "^2.5", + "guzzlehttp/psr7": "^2.12.3", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-client-implementation": "1.0" @@ -1294,8 +1295,9 @@ "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "guzzle/client-integration-tests": "3.0.2", + "guzzlehttp/test-server": "^0.5.1", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -1373,7 +1375,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + "source": "https://github.com/guzzle/guzzle/tree/7.12.3" }, "funding": [ { @@ -1389,28 +1391,29 @@ "type": "tidelift" } ], - "time": "2025-08-23T22:36:01+00:00" + "time": "2026-06-23T15:29:02+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.3.0", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "481557b130ef3790cf82b713667b43030dc9c957" + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", - "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e", + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e", "shasum": "" }, "require": { - "php": "^7.2.5 || ^8.0" + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.44 || ^9.6.25" + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "type": "library", "extra": { @@ -1456,7 +1459,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.3.0" + "source": "https://github.com/guzzle/promises/tree/2.5.0" }, "funding": [ { @@ -1472,27 +1475,29 @@ "type": "tidelift" } ], - "time": "2025-08-22T14:34:08+00:00" + "time": "2026-06-02T12:23:43+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.8.0", + "version": "2.12.3", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "21dc724a0583619cd1652f673303492272778051" + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", - "reference": "21dc724a0583619cd1652f673303492272778051", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7ec62dc3f44aa218487dbed81a9bf9bc647be55d", + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-factory-implementation": "1.0", @@ -1500,8 +1505,9 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.44 || ^9.6.25" + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -1572,7 +1578,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.8.0" + "source": "https://github.com/guzzle/psr7/tree/2.12.3" }, "funding": [ { @@ -1588,29 +1594,29 @@ "type": "tidelift" } ], - "time": "2025-08-23T21:21:41+00:00" + "time": "2026-06-23T15:21:08+00:00" }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.5", + "version": "v1.0.8", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + "reference": "9c19128923b05a5d7355e5d2318d7808b7e33bbd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", - "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/9c19128923b05a5d7355e5d2318d7808b7e33bbd", + "reference": "9c19128923b05a5d7355e5d2318d7808b7e33bbd", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "symfony/polyfill-php80": "^1.24" + "symfony/polyfill-php80": "^1.25" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", "uri-template/tests": "1.0.0" }, "type": "library", @@ -1658,7 +1664,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.8" }, "funding": [ { @@ -1674,20 +1680,62 @@ "type": "tidelift" } ], - "time": "2025-08-22T14:27:06+00:00" + "time": "2026-06-23T13:02:23+00:00" + }, + { + "name": "jumbojett/openid-connect-php", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/jumbojett/OpenID-Connect-PHP.git", + "reference": "f327e7eb0626d55ddb6abc7b7c9e6ad3af4e5d51" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jumbojett/OpenID-Connect-PHP/zipball/f327e7eb0626d55ddb6abc7b7c9e6ad3af4e5d51", + "reference": "f327e7eb0626d55ddb6abc7b7c9e6ad3af4e5d51", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "php": ">=7.0", + "phpseclib/phpseclib": "^3.0.7" + }, + "require-dev": { + "phpunit/phpunit": "<10", + "roave/security-advisories": "dev-latest", + "yoast/phpunit-polyfills": "^2.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "Bare-bones OpenID Connect client", + "support": { + "issues": "https://github.com/jumbojett/OpenID-Connect-PHP/issues", + "source": "https://github.com/jumbojett/OpenID-Connect-PHP/tree/v1.0.2" + }, + "time": "2024-09-13T07:08:11+00:00" }, { "name": "laravel/framework", - "version": "v12.53.0", + "version": "v12.62.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "f57f035c0d34503d9ff30be76159bb35a003cd1f" + "reference": "f7e61eb1e0e06a38996802b769bce9127aec227c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/f57f035c0d34503d9ff30be76159bb35a003cd1f", - "reference": "f57f035c0d34503d9ff30be76159bb35a003cd1f", + "url": "https://api.github.com/repos/laravel/framework/zipball/f7e61eb1e0e06a38996802b769bce9127aec227c", + "reference": "f7e61eb1e0e06a38996802b769bce9127aec227c", "shasum": "" }, "require": { @@ -1708,7 +1756,7 @@ "guzzlehttp/uri-template": "^1.0", "laravel/prompts": "^0.3.0", "laravel/serializable-closure": "^1.3|^2.0", - "league/commonmark": "^2.7", + "league/commonmark": "^2.8.1", "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", "league/uri": "^7.5.1", @@ -1728,8 +1776,8 @@ "symfony/mailer": "^7.2.0", "symfony/mime": "^7.2.0", "symfony/polyfill-php83": "^1.33", - "symfony/polyfill-php84": "^1.33", - "symfony/polyfill-php85": "^1.33", + "symfony/polyfill-php84": "^1.34", + "symfony/polyfill-php85": "^1.34", "symfony/process": "^7.2.0", "symfony/routing": "^7.2.0", "symfony/uid": "^7.2.0", @@ -1803,7 +1851,7 @@ "orchestra/testbench-core": "^10.9.0", "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", - "phpstan/phpstan": "^2.0", + "phpstan/phpstan": "^2.1.41", "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", "predis/predis": "^2.3|^3.0", "resend/resend-php": "^0.10.0|^1.0", @@ -1896,24 +1944,24 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-02-24T14:35:15+00:00" + "time": "2026-06-09T13:50:13+00:00" }, { "name": "laravel/helpers", - "version": "v1.8.2", + "version": "v1.8.3", "source": { "type": "git", "url": "https://github.com/laravel/helpers.git", - "reference": "98499eea4c1cca76fb0fb37ed365a468773daf0a" + "reference": "5915be977c7cc05fe2498d561b8c026ee56567dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/helpers/zipball/98499eea4c1cca76fb0fb37ed365a468773daf0a", - "reference": "98499eea4c1cca76fb0fb37ed365a468773daf0a", + "url": "https://api.github.com/repos/laravel/helpers/zipball/5915be977c7cc05fe2498d561b8c026ee56567dd", + "reference": "5915be977c7cc05fe2498d561b8c026ee56567dd", "shasum": "" }, "require": { - "illuminate/support": "~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", "php": "^7.2.0|^8.0" }, "require-dev": { @@ -1951,22 +1999,22 @@ "laravel" ], "support": { - "source": "https://github.com/laravel/helpers/tree/v1.8.2" + "source": "https://github.com/laravel/helpers/tree/v1.8.3" }, - "time": "2025-11-25T14:46:28+00:00" + "time": "2026-03-17T16:40:11+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.13", + "version": "v0.3.20", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "ed8c466571b37e977532fb2fd3c272c784d7050d" + "reference": "6d53c826dfd33ec5f4ac91f816ff327727c7988b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/ed8c466571b37e977532fb2fd3c272c784d7050d", - "reference": "ed8c466571b37e977532fb2fd3c272c784d7050d", + "url": "https://api.github.com/repos/laravel/prompts/zipball/6d53c826dfd33ec5f4ac91f816ff327727c7988b", + "reference": "6d53c826dfd33ec5f4ac91f816ff327727c7988b", "shasum": "" }, "require": { @@ -2010,22 +2058,22 @@ "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.13" + "source": "https://github.com/laravel/prompts/tree/v0.3.20" }, - "time": "2026-02-06T12:17:10+00:00" + "time": "2026-06-24T19:16:38+00:00" }, { "name": "laravel/reverb", - "version": "v1.8.0", + "version": "v1.10.2", "source": { "type": "git", "url": "https://github.com/laravel/reverb.git", - "reference": "53753b72035f1b13899fa57d2ad4dfe9480c8d61" + "reference": "43a5c0a99b1aaba33dc32f97fcf51f182dd8c8ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/reverb/zipball/53753b72035f1b13899fa57d2ad4dfe9480c8d61", - "reference": "53753b72035f1b13899fa57d2ad4dfe9480c8d61", + "url": "https://api.github.com/repos/laravel/reverb/zipball/43a5c0a99b1aaba33dc32f97fcf51f182dd8c8ac", + "reference": "43a5c0a99b1aaba33dc32f97fcf51f182dd8c8ac", "shasum": "" }, "require": { @@ -2089,22 +2137,22 @@ ], "support": { "issues": "https://github.com/laravel/reverb/issues", - "source": "https://github.com/laravel/reverb/tree/v1.8.0" + "source": "https://github.com/laravel/reverb/tree/v1.10.2" }, - "time": "2026-02-21T14:37:48+00:00" + "time": "2026-05-10T15:47:52+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.10", + "version": "v2.0.13", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669" + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669", - "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", "shasum": "" }, "require": { @@ -2152,7 +2200,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-02-20T19:59:49+00:00" + "time": "2026-04-16T14:03:50+00:00" }, { "name": "laravel/tinker", @@ -2222,29 +2270,29 @@ }, { "name": "laravel/ui", - "version": "v4.6.1", + "version": "v4.6.3", "source": { "type": "git", "url": "https://github.com/laravel/ui.git", - "reference": "7d6ffa38d79f19c9b3e70a751a9af845e8f41d88" + "reference": "ff27db15416c1ed8ad9848f5692e47595dd5de27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/ui/zipball/7d6ffa38d79f19c9b3e70a751a9af845e8f41d88", - "reference": "7d6ffa38d79f19c9b3e70a751a9af845e8f41d88", + "url": "https://api.github.com/repos/laravel/ui/zipball/ff27db15416c1ed8ad9848f5692e47595dd5de27", + "reference": "ff27db15416c1ed8ad9848f5692e47595dd5de27", "shasum": "" }, "require": { - "illuminate/console": "^9.21|^10.0|^11.0|^12.0", - "illuminate/filesystem": "^9.21|^10.0|^11.0|^12.0", - "illuminate/support": "^9.21|^10.0|^11.0|^12.0", - "illuminate/validation": "^9.21|^10.0|^11.0|^12.0", + "illuminate/console": "^9.21|^10.0|^11.0|^12.0|^13.0", + "illuminate/filesystem": "^9.21|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^9.21|^10.0|^11.0|^12.0|^13.0", + "illuminate/validation": "^9.21|^10.0|^11.0|^12.0|^13.0", "php": "^8.0", - "symfony/console": "^6.0|^7.0" + "symfony/console": "^6.0|^7.0|^8.0" }, "require-dev": { - "orchestra/testbench": "^7.35|^8.15|^9.0|^10.0", - "phpunit/phpunit": "^9.3|^10.4|^11.5" + "orchestra/testbench": "^7.35|^8.15|^9.0|^10.0|^11.0", + "phpunit/phpunit": "^9.3|^10.4|^11.5|^12.5|^13.0" }, "type": "library", "extra": { @@ -2279,9 +2327,9 @@ "ui" ], "support": { - "source": "https://github.com/laravel/ui/tree/v4.6.1" + "source": "https://github.com/laravel/ui/tree/v4.6.3" }, - "time": "2025-01-28T15:15:29+00:00" + "time": "2026-03-17T13:41:52+00:00" }, { "name": "lcobucci/jwt", @@ -2358,16 +2406,16 @@ }, { "name": "league/commonmark", - "version": "2.8.0", + "version": "2.8.2", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb" + "reference": "59fb075d2101740c337c7216e3f32b36c204218b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/4efa10c1e56488e658d10adf7b7b7dcd19940bfb", - "reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b", "shasum": "" }, "require": { @@ -2392,9 +2440,9 @@ "phpstan/phpstan": "^1.8.2", "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", "scrutinizer/ocular": "^1.8.1", - "symfony/finder": "^5.3 | ^6.0 | ^7.0", - "symfony/process": "^5.4 | ^6.0 | ^7.0", - "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", "unleashedtech/php-coding-standard": "^3.1.1", "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" }, @@ -2461,7 +2509,7 @@ "type": "tidelift" } ], - "time": "2025-11-26T21:48:24+00:00" + "time": "2026-03-19T13:16:38+00:00" }, { "name": "league/config", @@ -2638,16 +2686,16 @@ }, { "name": "league/flysystem", - "version": "3.32.0", + "version": "3.35.1", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725" + "reference": "f23af6c5aafd958a7593029a271d77baf5ed793c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/254b1595b16b22dbddaaef9ed6ca9fdac4956725", - "reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f23af6c5aafd958a7593029a271d77baf5ed793c", + "reference": "f23af6c5aafd958a7593029a271d77baf5ed793c", "shasum": "" }, "require": { @@ -2715,9 +2763,9 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.32.0" + "source": "https://github.com/thephpleague/flysystem/tree/3.35.1" }, - "time": "2026-02-25T17:01:41+00:00" + "time": "2026-06-25T06:52:23+00:00" }, { "name": "league/flysystem-local", @@ -2891,20 +2939,20 @@ }, { "name": "league/uri", - "version": "7.8.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "4436c6ec8d458e4244448b069cc572d088230b76" + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76", - "reference": "4436c6ec8d458e4244448b069cc572d088230b76", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.8", + "league/uri-interfaces": "^7.8.1", "php": "^8.1", "psr/http-factory": "^1" }, @@ -2977,7 +3025,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.8.0" + "source": "https://github.com/thephpleague/uri/tree/7.8.1" }, "funding": [ { @@ -2985,20 +3033,20 @@ "type": "github" } ], - "time": "2026-01-14T17:24:56+00:00" + "time": "2026-03-15T20:22:25+00:00" }, { "name": "league/uri-interfaces", - "version": "7.8.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4" + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c5c5cd056110fc8afaba29fa6b72a43ced42acd4", - "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", "shasum": "" }, "require": { @@ -3061,7 +3109,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" }, "funding": [ { @@ -3069,7 +3117,7 @@ "type": "github" } ], - "time": "2026-01-15T06:54:53+00:00" + "time": "2026-03-08T20:05:35+00:00" }, { "name": "mervick/aes-everywhere", @@ -3284,16 +3332,16 @@ }, { "name": "nesbot/carbon", - "version": "3.11.1", + "version": "3.13.0", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "f438fcc98f92babee98381d399c65336f3a3827f" + "reference": "40f6618f052df16b545f626fbf9a878e6497d16a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/f438fcc98f92babee98381d399c65336f3a3827f", - "reference": "f438fcc98f92babee98381d399c65336f3a3827f", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/40f6618f052df16b545f626fbf9a878e6497d16a", + "reference": "40f6618f052df16b545f626fbf9a878e6497d16a", "shasum": "" }, "require": { @@ -3385,7 +3433,7 @@ "type": "tidelift" } ], - "time": "2026-01-29T09:26:29+00:00" + "time": "2026-06-18T13:49:15+00:00" }, { "name": "nette/schema", @@ -3456,16 +3504,16 @@ }, { "name": "nette/utils", - "version": "v4.1.3", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe" + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/bb3ea637e3d131d72acc033cfc2746ee893349fe", - "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe", + "url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7", "shasum": "" }, "require": { @@ -3541,9 +3589,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.1.3" + "source": "https://github.com/nette/utils/tree/v4.1.4" }, - "time": "2026-02-13T03:05:33+00:00" + "time": "2026-05-11T20:49:54+00:00" }, { "name": "nikic/php-parser", @@ -3809,133 +3857,37 @@ }, "time": "2020-10-15T08:29:30+00:00" }, - { - "name": "paragonie/sodium_compat", - "version": "v2.5.0", - "source": { - "type": "git", - "url": "https://github.com/paragonie/sodium_compat.git", - "reference": "4714da6efdc782c06690bc72ce34fae7941c2d9f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/4714da6efdc782c06690bc72ce34fae7941c2d9f", - "reference": "4714da6efdc782c06690bc72ce34fae7941c2d9f", - "shasum": "" - }, - "require": { - "php": "^8.1", - "php-64bit": "*" - }, - "require-dev": { - "infection/infection": "^0", - "nikic/php-fuzzer": "^0", - "phpunit/phpunit": "^7|^8|^9|^10|^11", - "vimeo/psalm": "^4|^5|^6" - }, - "suggest": { - "ext-sodium": "Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "files": [ - "autoload.php" - ], - "psr-4": { - "ParagonIE\\Sodium\\": "namespaced/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "ISC" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com" - }, - { - "name": "Frank Denis", - "email": "jedisct1@pureftpd.org" - } - ], - "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", - "keywords": [ - "Authentication", - "BLAKE2b", - "ChaCha20", - "ChaCha20-Poly1305", - "Chapoly", - "Curve25519", - "Ed25519", - "EdDSA", - "Edwards-curve Digital Signature Algorithm", - "Elliptic Curve Diffie-Hellman", - "Poly1305", - "Pure-PHP cryptography", - "RFC 7748", - "RFC 8032", - "Salpoly", - "Salsa20", - "X25519", - "XChaCha20-Poly1305", - "XSalsa20-Poly1305", - "Xchacha20", - "Xsalsa20", - "aead", - "cryptography", - "ecdh", - "elliptic curve", - "elliptic curve cryptography", - "encryption", - "libsodium", - "php", - "public-key cryptography", - "secret-key cryptography", - "side-channel resistant" - ], - "support": { - "issues": "https://github.com/paragonie/sodium_compat/issues", - "source": "https://github.com/paragonie/sodium_compat/tree/v2.5.0" - }, - "time": "2025-12-30T16:12:18+00:00" - }, { "name": "php-open-source-saver/jwt-auth", - "version": "v2.8.3", + "version": "2.9.2", "source": { "type": "git", "url": "https://github.com/PHP-Open-Source-Saver/jwt-auth.git", - "reference": "563f7dc025f48b9ecbacc271da509bbb4c6b3b23" + "reference": "ce08363a9986e5253efd3663ed4f75c976bec89a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-Open-Source-Saver/jwt-auth/zipball/563f7dc025f48b9ecbacc271da509bbb4c6b3b23", - "reference": "563f7dc025f48b9ecbacc271da509bbb4c6b3b23", + "url": "https://api.github.com/repos/PHP-Open-Source-Saver/jwt-auth/zipball/ce08363a9986e5253efd3663ed4f75c976bec89a", + "reference": "ce08363a9986e5253efd3663ed4f75c976bec89a", "shasum": "" }, "require": { "ext-json": "*", - "illuminate/auth": "^10|^11|^12", - "illuminate/contracts": "^10|^11|^12", - "illuminate/http": "^10|^11|^12", - "illuminate/support": "^10|^11|^12", + "illuminate/auth": "^12|^13", + "illuminate/contracts": "^12|^13", + "illuminate/http": "^12|^13", + "illuminate/support": "^12|^13", "lcobucci/jwt": "^5.4", "namshi/jose": "^7.0", "nesbot/carbon": "^2.0|^3.0", - "php": "^8.2" + "php": "^8.3" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3", - "illuminate/console": "^10|^11|^12", - "illuminate/routing": "^10|^11|^12", + "illuminate/console": "^12|^13", + "illuminate/routing": "^12|^13", "mockery/mockery": "^1.6", - "orchestra/testbench": "^8|^9|^10", + "orchestra/testbench": "^10|^11", "phpstan/phpstan": "^2", "phpunit/phpunit": "^10.5|^11" }, @@ -3949,9 +3901,6 @@ "providers": [ "PHPOpenSourceSaver\\JWTAuth\\Providers\\LaravelServiceProvider" ] - }, - "branch-alias": { - "dev-develop": "2.0-dev" } }, "autoload": { @@ -3999,7 +3948,7 @@ "issues": "https://github.com/PHP-Open-Source-Saver/jwt-auth/issues", "source": "https://github.com/PHP-Open-Source-Saver/jwt-auth" }, - "time": "2025-10-15T12:02:51+00:00" + "time": "2026-05-07T16:44:01+00:00" }, { "name": "phpoption/phpoption", @@ -4078,16 +4027,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.49", + "version": "3.0.55", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9" + "reference": "db9744e6d47e742b1f974e965ad49bdd041105af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6233a1e12584754e6b5daa69fe1289b47775c1b9", - "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/db9744e6d47e742b1f974e965ad49bdd041105af", + "reference": "db9744e6d47e742b1f974e965ad49bdd041105af", "shasum": "" }, "require": { @@ -4168,7 +4117,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.49" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.55" }, "funding": [ { @@ -4184,7 +4133,7 @@ "type": "tidelift" } ], - "time": "2026-01-27T09:17:28+00:00" + "time": "2026-06-14T23:24:10+00:00" }, { "name": "pragmarx/google2fa", @@ -4240,16 +4189,16 @@ }, { "name": "pragmarx/google2fa-laravel", - "version": "v2.3.0", + "version": "v2.3.1", "source": { "type": "git", "url": "https://github.com/antonioribeiro/google2fa-laravel.git", - "reference": "60f363c16db1e94263e0560efebe9fc2e302b7ef" + "reference": "7b8c5db50c0c86ae66470d3657d7382ac3574cab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/google2fa-laravel/zipball/60f363c16db1e94263e0560efebe9fc2e302b7ef", - "reference": "60f363c16db1e94263e0560efebe9fc2e302b7ef", + "url": "https://api.github.com/repos/antonioribeiro/google2fa-laravel/zipball/7b8c5db50c0c86ae66470d3657d7382ac3574cab", + "reference": "7b8c5db50c0c86ae66470d3657d7382ac3574cab", "shasum": "" }, "require": { @@ -4310,9 +4259,9 @@ ], "support": { "issues": "https://github.com/antonioribeiro/google2fa-laravel/issues", - "source": "https://github.com/antonioribeiro/google2fa-laravel/tree/v2.3.0" + "source": "https://github.com/antonioribeiro/google2fa-laravel/tree/v2.3.1" }, - "time": "2025-02-26T19:39:35+00:00" + "time": "2026-03-17T21:00:23+00:00" }, { "name": "pragmarx/google2fa-qrcode", @@ -4906,16 +4855,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.20", + "version": "v0.12.23", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "19678eb6b952a03b8a1d96ecee9edba518bb0373" + "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/19678eb6b952a03b8a1d96ecee9edba518bb0373", - "reference": "19678eb6b952a03b8a1d96ecee9edba518bb0373", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4dcc0f08047d52bbde475eda481146fd8e27e1a4", + "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4", "shasum": "" }, "require": { @@ -4979,29 +4928,28 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.20" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.23" }, - "time": "2026-02-11T15:05:28+00:00" + "time": "2026-05-23T13:41:31+00:00" }, { "name": "pusher/pusher-php-server", - "version": "7.2.7", + "version": "7.2.8", "source": { "type": "git", "url": "https://github.com/pusher/pusher-http-php.git", - "reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7" + "reference": "4aa139ed2a2a805cd265449b691198beee1309d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/148b0b5100d000ed57195acdf548a2b1b38ee3f7", - "reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7", + "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/4aa139ed2a2a805cd265449b691198beee1309d2", + "reference": "4aa139ed2a2a805cd265449b691198beee1309d2", "shasum": "" }, "require": { "ext-curl": "*", "ext-json": "*", "guzzlehttp/guzzle": "^7.2", - "paragonie/sodium_compat": "^1.6|^2.0", "php": "^7.3|^8.0", "psr/log": "^1.0|^2.0|^3.0" }, @@ -5040,9 +4988,9 @@ ], "support": { "issues": "https://github.com/pusher/pusher-http-php/issues", - "source": "https://github.com/pusher/pusher-http-php/tree/7.2.7" + "source": "https://github.com/pusher/pusher-http-php/tree/7.2.8" }, - "time": "2025-01-06T10:56:20+00:00" + "time": "2026-05-18T13:11:36+00:00" }, { "name": "ralouphie/getallheaders", @@ -5166,20 +5114,20 @@ }, { "name": "ramsey/uuid", - "version": "4.9.2", + "version": "4.9.3", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "8429c78ca35a09f27565311b98101e2826affde0" + "reference": "1df15849d00943a67d677dc9cfd80795f038c9f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", - "reference": "8429c78ca35a09f27565311b98101e2826affde0", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/1df15849d00943a67d677dc9cfd80795f038c9f8", + "reference": "1df15849d00943a67d677dc9cfd80795f038c9f8", "shasum": "" }, "require": { - "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "brick/math": ">=0.8.16 <=0.18", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -5238,22 +5186,22 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.9.2" + "source": "https://github.com/ramsey/uuid/tree/4.9.3" }, - "time": "2025-12-14T04:43:48+00:00" + "time": "2026-06-18T03:57:49+00:00" }, { "name": "ratchet/rfc6455", - "version": "v0.4.0", + "version": "v0.4.1", "source": { "type": "git", "url": "https://github.com/ratchetphp/RFC6455.git", - "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef" + "reference": "9b05f371219cbaf9748b505f139617dd0715592b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/859d95f85dda0912c6d5b936d036d044e3af47ef", - "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef", + "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/9b05f371219cbaf9748b505f139617dd0715592b", + "reference": "9b05f371219cbaf9748b505f139617dd0715592b", "shasum": "" }, "require": { @@ -5297,9 +5245,9 @@ "support": { "chat": "https://gitter.im/reactphp/reactphp", "issues": "https://github.com/ratchetphp/RFC6455/issues", - "source": "https://github.com/ratchetphp/RFC6455/tree/v0.4.0" + "source": "https://github.com/ratchetphp/RFC6455/tree/v0.4.1" }, - "time": "2025-02-24T01:18:22+00:00" + "time": "2026-06-06T14:34:23+00:00" }, { "name": "react/cache", @@ -5953,20 +5901,20 @@ }, { "name": "symfony/clock", - "version": "v8.0.0", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f" + "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/832119f9b8dbc6c8e6f65f30c5969eca1e88764f", - "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "url": "https://api.github.com/repos/symfony/clock/zipball/701ef4de9705d6c32292ebee5e8044094a09fbf6", + "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "psr/clock": "^1.0" }, "provide": { @@ -6006,7 +5954,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v8.0.0" + "source": "https://github.com/symfony/clock/tree/v8.1.0" }, "funding": [ { @@ -6026,20 +5974,20 @@ "type": "tidelift" } ], - "time": "2025-11-12T15:46:48+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/console", - "version": "v7.4.6", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "6d643a93b47398599124022eb24d97c153c12f27" + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/6d643a93b47398599124022eb24d97c153c12f27", - "reference": "6d643a93b47398599124022eb24d97c153c12f27", + "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", "shasum": "" }, "require": { @@ -6104,7 +6052,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.6" + "source": "https://github.com/symfony/console/tree/v7.4.13" }, "funding": [ { @@ -6124,24 +6072,24 @@ "type": "tidelift" } ], - "time": "2026-02-25T17:02:47+00:00" + "time": "2026-05-24T08:56:14+00:00" }, { "name": "symfony/css-selector", - "version": "v8.0.6", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "2a178bf80f05dbbe469a337730eba79d61315262" + "reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/2a178bf80f05dbbe469a337730eba79d61315262", - "reference": "2a178bf80f05dbbe469a337730eba79d61315262", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/dc0e2be45c9b5588c82414f02ac574b4b986abcd", + "reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd", "shasum": "" }, "require": { - "php": ">=8.4" + "php": ">=8.4.1" }, "type": "library", "autoload": { @@ -6173,7 +6121,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v8.0.6" + "source": "https://github.com/symfony/css-selector/tree/v8.1.0" }, "funding": [ { @@ -6193,20 +6141,20 @@ "type": "tidelift" } ], - "time": "2026-02-17T13:07:04+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -6219,7 +6167,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -6244,7 +6192,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -6255,25 +6203,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "symfony/error-handler", - "version": "v7.4.4", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8" + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8", - "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", "shasum": "" }, "require": { @@ -6322,7 +6274,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.4.4" + "source": "https://github.com/symfony/error-handler/tree/v7.4.8" }, "funding": [ { @@ -6342,20 +6294,20 @@ "type": "tidelift" } ], - "time": "2026-01-20T16:42:42+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.4.4", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "dc2c0eba1af673e736bb851d747d266108aea746" + "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/dc2c0eba1af673e736bb851d747d266108aea746", - "reference": "dc2c0eba1af673e736bb851d747d266108aea746", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e4a2e29753c7801f7a8340e066cfa788f3bc8101", + "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101", "shasum": "" }, "require": { @@ -6407,7 +6359,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.4" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.9" }, "funding": [ { @@ -6427,20 +6379,20 @@ "type": "tidelift" } ], - "time": "2026-01-05T11:45:34+00:00" + "time": "2026-04-18T13:18:21+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", - "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", "shasum": "" }, "require": { @@ -6454,7 +6406,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -6487,7 +6439,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" }, "funding": [ { @@ -6498,25 +6450,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-01-05T13:30:16+00:00" }, { "name": "symfony/finder", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf" + "reference": "e0be088d22278583a82da281886e8c3592fbf149" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf", - "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", + "reference": "e0be088d22278583a82da281886e8c3592fbf149", "shasum": "" }, "require": { @@ -6551,7 +6507,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.6" + "source": "https://github.com/symfony/finder/tree/v7.4.8" }, "funding": [ { @@ -6571,20 +6527,20 @@ "type": "tidelift" } ], - "time": "2026-01-29T09:40:50+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.4.6", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "fd97d5e926e988a363cef56fbbf88c5c528e9065" + "reference": "bc354f47c62301e990b7874fa662326368508e2c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/fd97d5e926e988a363cef56fbbf88c5c528e9065", - "reference": "fd97d5e926e988a363cef56fbbf88c5c528e9065", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bc354f47c62301e990b7874fa662326368508e2c", + "reference": "bc354f47c62301e990b7874fa662326368508e2c", "shasum": "" }, "require": { @@ -6633,7 +6589,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.4.6" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.13" }, "funding": [ { @@ -6653,20 +6609,20 @@ "type": "tidelift" } ], - "time": "2026-02-21T16:25:55+00:00" + "time": "2026-05-24T11:20:33+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.6", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "002ac0cf4cd972a7fd0912dcd513a95e8a81ce83" + "reference": "9df847980c436451f4f51d1284491bb4356dd989" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/002ac0cf4cd972a7fd0912dcd513a95e8a81ce83", - "reference": "002ac0cf4cd972a7fd0912dcd513a95e8a81ce83", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9df847980c436451f4f51d1284491bb4356dd989", + "reference": "9df847980c436451f4f51d1284491bb4356dd989", "shasum": "" }, "require": { @@ -6752,7 +6708,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.4.6" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.13" }, "funding": [ { @@ -6772,20 +6728,20 @@ "type": "tidelift" } ], - "time": "2026-02-26T08:30:57+00:00" + "time": "2026-05-27T08:31:43+00:00" }, { "name": "symfony/mailer", - "version": "v7.4.6", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9" + "reference": "5cefb712a25f320579615ba9e1942abaeade7dff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/b02726f39a20bc65e30364f5c750c4ddbf1f58e9", - "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9", + "url": "https://api.github.com/repos/symfony/mailer/zipball/5cefb712a25f320579615ba9e1942abaeade7dff", + "reference": "5cefb712a25f320579615ba9e1942abaeade7dff", "shasum": "" }, "require": { @@ -6836,7 +6792,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.4.6" + "source": "https://github.com/symfony/mailer/tree/v7.4.12" }, "funding": [ { @@ -6856,20 +6812,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:50:00+00:00" + "time": "2026-05-20T07:20:23+00:00" }, { "name": "symfony/mime", - "version": "v7.4.6", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "9fc881d95feae4c6c48678cb6372bd8a7ba04f5f" + "reference": "a845722765c4f6b2ce88beaf4f4479975b186770" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/9fc881d95feae4c6c48678cb6372bd8a7ba04f5f", - "reference": "9fc881d95feae4c6c48678cb6372bd8a7ba04f5f", + "url": "https://api.github.com/repos/symfony/mime/zipball/a845722765c4f6b2ce88beaf4f4479975b186770", + "reference": "a845722765c4f6b2ce88beaf4f4479975b186770", "shasum": "" }, "require": { @@ -6925,7 +6881,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.6" + "source": "https://github.com/symfony/mime/tree/v7.4.13" }, "funding": [ { @@ -6945,20 +6901,20 @@ "type": "tidelift" } ], - "time": "2026-02-05T15:57:06+00:00" + "time": "2026-05-23T16:22:37+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -7008,7 +6964,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -7028,20 +6984,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", "shasum": "" }, "require": { @@ -7090,7 +7046,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" }, "funding": [ { @@ -7110,20 +7066,20 @@ "type": "tidelift" } ], - "time": "2025-06-27T09:58:17+00:00" + "time": "2026-05-26T05:58:03+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.33.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + "reference": "dc21118016c039a66235cf93d96b435ffb282412" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412", + "reference": "dc21118016c039a66235cf93d96b435ffb282412", "shasum": "" }, "require": { @@ -7177,7 +7133,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1" }, "funding": [ { @@ -7197,20 +7153,20 @@ "type": "tidelift" } ], - "time": "2024-09-10T14:38:51+00:00" + "time": "2026-05-25T15:22:23+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "shasum": "" }, "require": { @@ -7262,7 +7218,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" }, "funding": [ { @@ -7282,20 +7238,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-25T13:48:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { @@ -7347,7 +7303,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -7367,7 +7323,7 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-05-27T06:59:30+00:00" }, { "name": "symfony/polyfill-php56", @@ -7439,16 +7395,16 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", "shasum": "" }, "require": { @@ -7499,7 +7455,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" }, "funding": [ { @@ -7519,20 +7475,20 @@ "type": "tidelift" } ], - "time": "2025-01-02T08:10:11+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.33.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/796a26abb75ce49f3a84433cd81bf1009d73d5f8", + "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8", "shasum": "" }, "require": { @@ -7579,7 +7535,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.2" }, "funding": [ { @@ -7599,20 +7555,20 @@ "type": "tidelift" } ], - "time": "2025-07-08T02:45:35+00:00" + "time": "2026-05-27T06:51:48+00:00" }, { "name": "symfony/polyfill-php84", - "version": "v1.33.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", "shasum": "" }, "require": { @@ -7659,7 +7615,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" }, "funding": [ { @@ -7679,20 +7635,20 @@ "type": "tidelift" } ], - "time": "2025-06-24T13:30:11+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { "name": "symfony/polyfill-php85", - "version": "v1.33.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", - "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", "shasum": "" }, "require": { @@ -7739,7 +7695,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1" }, "funding": [ { @@ -7759,20 +7715,20 @@ "type": "tidelift" } ], - "time": "2025-06-23T16:12:55+00:00" + "time": "2026-05-26T02:25:22+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", - "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94", "shasum": "" }, "require": { @@ -7822,7 +7778,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0" }, "funding": [ { @@ -7842,20 +7798,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/process", - "version": "v7.4.5", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "608476f4604102976d687c483ac63a79ba18cc97" + "reference": "f5804be144caceb570f6747519999636b664f24c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", - "reference": "608476f4604102976d687c483ac63a79ba18cc97", + "url": "https://api.github.com/repos/symfony/process/zipball/f5804be144caceb570f6747519999636b664f24c", + "reference": "f5804be144caceb570f6747519999636b664f24c", "shasum": "" }, "require": { @@ -7887,7 +7843,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.5" + "source": "https://github.com/symfony/process/tree/v7.4.13" }, "funding": [ { @@ -7907,20 +7863,20 @@ "type": "tidelift" } ], - "time": "2026-01-26T15:07:59+00:00" + "time": "2026-05-23T16:05:06+00:00" }, { "name": "symfony/routing", - "version": "v7.4.6", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "238d749c56b804b31a9bf3e26519d93b65a60938" + "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/238d749c56b804b31a9bf3e26519d93b65a60938", - "reference": "238d749c56b804b31a9bf3e26519d93b65a60938", + "url": "https://api.github.com/repos/symfony/routing/zipball/3a162171bb008e5e0f15dce6581373a4c0e8390d", + "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d", "shasum": "" }, "require": { @@ -7972,7 +7928,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.6" + "source": "https://github.com/symfony/routing/tree/v7.4.13" }, "funding": [ { @@ -7992,20 +7948,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:50:00+00:00" + "time": "2026-05-24T11:20:33+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { @@ -8023,7 +7979,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -8059,7 +8015,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { @@ -8079,24 +8035,24 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:30:57+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { "name": "symfony/string", - "version": "v8.0.6", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" + "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", - "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "url": "https://api.github.com/repos/symfony/string/zipball/afd5944f4005862d961efb85c8bbd5c523c4e3c9", + "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-intl-grapheme": "^1.33", "symfony/polyfill-intl-normalizer": "^1.0", @@ -8149,7 +8105,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.6" + "source": "https://github.com/symfony/string/tree/v8.1.0" }, "funding": [ { @@ -8169,24 +8125,24 @@ "type": "tidelift" } ], - "time": "2026-02-09T10:14:57+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/translation", - "version": "v8.0.6", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b" + "reference": "b2bd012ca28c4acae830ee1206a5b6e35dd99693" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b", - "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b", + "url": "https://api.github.com/repos/symfony/translation/zipball/b2bd012ca28c4acae830ee1206a5b6e35dd99693", + "reference": "b2bd012ca28c4acae830ee1206a5b6e35dd99693", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/polyfill-mbstring": "^1.0", "symfony/translation-contracts": "^3.6.1" }, @@ -8242,7 +8198,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v8.0.6" + "source": "https://github.com/symfony/translation/tree/v8.1.0" }, "funding": [ { @@ -8262,20 +8218,20 @@ "type": "tidelift" } ], - "time": "2026-02-17T13:07:04+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.6.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "65a8bc82080447fae78373aa10f8d13b38338977" + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", - "reference": "65a8bc82080447fae78373aa10f8d13b38338977", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", "shasum": "" }, "require": { @@ -8288,7 +8244,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -8324,7 +8280,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0" }, "funding": [ { @@ -8344,20 +8300,20 @@ "type": "tidelift" } ], - "time": "2025-07-15T13:41:35+00:00" + "time": "2026-01-05T13:30:16+00:00" }, { "name": "symfony/uid", - "version": "v7.4.4", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36" + "reference": "2676b524340abcfe4d6151ec698463cebafee439" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/7719ce8aba76be93dfe249192f1fbfa52c588e36", - "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36", + "url": "https://api.github.com/repos/symfony/uid/zipball/2676b524340abcfe4d6151ec698463cebafee439", + "reference": "2676b524340abcfe4d6151ec698463cebafee439", "shasum": "" }, "require": { @@ -8402,7 +8358,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.4.4" + "source": "https://github.com/symfony/uid/tree/v7.4.9" }, "funding": [ { @@ -8422,20 +8378,20 @@ "type": "tidelift" } ], - "time": "2026-01-03T23:30:35+00:00" + "time": "2026-04-30T15:19:22+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291" + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291", - "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd", "shasum": "" }, "require": { @@ -8489,7 +8445,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.6" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.8" }, "funding": [ { @@ -8509,7 +8465,7 @@ "type": "tidelift" } ], - "time": "2026-02-15T10:53:20+00:00" + "time": "2026-03-30T13:44:50+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -8652,23 +8608,23 @@ }, { "name": "voku/portable-ascii", - "version": "2.0.3", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/voku/portable-ascii.git", - "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + "reference": "8e1051fe39379367aecf014f41744ce7539a856f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", - "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f", "shasum": "" }, "require": { - "php": ">=7.0.0" + "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" }, "suggest": { "ext-intl": "Use Intl for transliterator_transliterate() support" @@ -8698,7 +8654,7 @@ ], "support": { "issues": "https://github.com/voku/portable-ascii/issues", - "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + "source": "https://github.com/voku/portable-ascii/tree/2.1.1" }, "funding": [ { @@ -8722,7 +8678,7 @@ "type": "tidelift" } ], - "time": "2024-11-21T01:49:47+00:00" + "time": "2026-04-26T05:33:54+00:00" } ], "packages-dev": [ @@ -8913,16 +8869,16 @@ }, { "name": "laravel/pint", - "version": "v1.27.1", + "version": "v1.29.3", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5" + "reference": "da1d1111a6aa2e082d2a388b194afe1ba0a05d14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/54cca2de13790570c7b6f0f94f37896bee4abcb5", - "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5", + "url": "https://api.github.com/repos/laravel/pint/zipball/da1d1111a6aa2e082d2a388b194afe1ba0a05d14", + "reference": "da1d1111a6aa2e082d2a388b194afe1ba0a05d14", "shasum": "" }, "require": { @@ -8933,13 +8889,14 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.93.1", - "illuminate/view": "^12.51.0", - "larastan/larastan": "^3.9.2", - "laravel-zero/framework": "^12.0.5", + "friendsofphp/php-cs-fixer": "^3.95.8", + "illuminate/view": "^12.62.0", + "larastan/larastan": "^3.10.0", + "laravel-zero/framework": "^12.1.0", + "laravel/agent-detector": "^2.0.2", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3.3", - "pestphp/pest": "^3.8.5" + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6" }, "bin": [ "builds/pint" @@ -8976,20 +8933,20 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2026-02-10T20:00:20+00:00" + "time": "2026-06-16T15:34:04+00:00" }, { "name": "laravel/sail", - "version": "v1.53.0", + "version": "v1.63.0", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "e340eaa2bea9b99192570c48ed837155dbf24fbb" + "reference": "51bbce3f803c1d386cabbb44e618c955a12ff5fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/e340eaa2bea9b99192570c48ed837155dbf24fbb", - "reference": "e340eaa2bea9b99192570c48ed837155dbf24fbb", + "url": "https://api.github.com/repos/laravel/sail/zipball/51bbce3f803c1d386cabbb44e618c955a12ff5fc", + "reference": "51bbce3f803c1d386cabbb44e618c955a12ff5fc", "shasum": "" }, "require": { @@ -9039,7 +8996,7 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2026-02-06T12:16:02+00:00" + "time": "2026-06-18T08:54:14+00:00" }, { "name": "mockery/mockery", @@ -9186,23 +9143,23 @@ }, { "name": "nunomaduro/collision", - "version": "v8.9.1", + "version": "v8.9.4", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935" + "reference": "716af8f95a470e9094cfca09ed897b023be191a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", - "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5", "shasum": "" }, "require": { "filp/whoops": "^2.18.4", "nunomaduro/termwind": "^2.4.0", "php": "^8.2.0", - "symfony/console": "^7.4.4 || ^8.0.4" + "symfony/console": "^7.4.8 || ^8.0.8" }, "conflict": { "laravel/framework": "<11.48.0 || >=14.0.0", @@ -9210,12 +9167,12 @@ }, "require-dev": { "brianium/paratest": "^7.8.5", - "larastan/larastan": "^3.9.2", - "laravel/framework": "^11.48.0 || ^12.52.0", - "laravel/pint": "^1.27.1", - "orchestra/testbench-core": "^9.12.0 || ^10.9.0", - "pestphp/pest": "^3.8.5 || ^4.4.1 || ^5.0.0", - "sebastian/environment": "^7.2.1 || ^8.0.3 || ^9.0.0" + "larastan/larastan": "^3.9.6", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0", + "laravel/pint": "^1.29.1", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1", + "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0" }, "type": "library", "extra": { @@ -9278,7 +9235,7 @@ "type": "patreon" } ], - "time": "2026-02-17T17:33:08+00:00" + "time": "2026-04-21T14:04:20+00:00" }, { "name": "phar-io/manifest", @@ -10843,16 +10800,16 @@ }, { "name": "spatie/backtrace", - "version": "1.8.1", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/spatie/backtrace.git", - "reference": "8c0f16a59ae35ec8c62d85c3c17585158f430110" + "reference": "8ffe78be5ed355b5009e3dd989d183433e9a5adc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/backtrace/zipball/8c0f16a59ae35ec8c62d85c3c17585158f430110", - "reference": "8c0f16a59ae35ec8c62d85c3c17585158f430110", + "url": "https://api.github.com/repos/spatie/backtrace/zipball/8ffe78be5ed355b5009e3dd989d183433e9a5adc", + "reference": "8ffe78be5ed355b5009e3dd989d183433e9a5adc", "shasum": "" }, "require": { @@ -10863,7 +10820,7 @@ "laravel/serializable-closure": "^1.3 || ^2.0", "phpunit/phpunit": "^9.3 || ^11.4.3", "spatie/phpunit-snapshot-assertions": "^4.2 || ^5.1.6", - "symfony/var-dumper": "^5.1 || ^6.0 || ^7.0" + "symfony/var-dumper": "^5.1|^6.0|^7.0|^8.0" }, "type": "library", "autoload": { @@ -10891,7 +10848,7 @@ ], "support": { "issues": "https://github.com/spatie/backtrace/issues", - "source": "https://github.com/spatie/backtrace/tree/1.8.1" + "source": "https://github.com/spatie/backtrace/tree/1.8.2" }, "funding": [ { @@ -10903,7 +10860,7 @@ "type": "other" } ], - "time": "2025-08-26T08:22:30+00:00" + "time": "2026-03-11T13:48:28+00:00" }, { "name": "spatie/error-solutions", @@ -10981,26 +10938,26 @@ }, { "name": "spatie/flare-client-php", - "version": "1.10.1", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/spatie/flare-client-php.git", - "reference": "bf1716eb98bd689451b071548ae9e70738dce62f" + "reference": "53f41b08a27cc039e1a8ed2be9a202e924f31bad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/bf1716eb98bd689451b071548ae9e70738dce62f", - "reference": "bf1716eb98bd689451b071548ae9e70738dce62f", + "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/53f41b08a27cc039e1a8ed2be9a202e924f31bad", + "reference": "53f41b08a27cc039e1a8ed2be9a202e924f31bad", "shasum": "" }, "require": { - "illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", "php": "^8.0", "spatie/backtrace": "^1.6.1", - "symfony/http-foundation": "^5.2|^6.0|^7.0", - "symfony/mime": "^5.2|^6.0|^7.0", - "symfony/process": "^5.2|^6.0|^7.0", - "symfony/var-dumper": "^5.2|^6.0|^7.0" + "symfony/http-foundation": "^5.2|^6.0|^7.0|^8.0", + "symfony/mime": "^5.2|^6.0|^7.0|^8.0", + "symfony/process": "^5.2|^6.0|^7.0|^8.0", + "symfony/var-dumper": "^5.2|^6.0|^7.0|^8.0" }, "require-dev": { "dms/phpunit-arraysubset-asserts": "^0.5.0", @@ -11038,7 +10995,7 @@ ], "support": { "issues": "https://github.com/spatie/flare-client-php/issues", - "source": "https://github.com/spatie/flare-client-php/tree/1.10.1" + "source": "https://github.com/spatie/flare-client-php/tree/1.11.1" }, "funding": [ { @@ -11046,41 +11003,44 @@ "type": "github" } ], - "time": "2025-02-14T13:42:06+00:00" + "time": "2026-05-15T09:31:32+00:00" }, { "name": "spatie/ignition", - "version": "1.15.1", + "version": "1.16.0", "source": { "type": "git", "url": "https://github.com/spatie/ignition.git", - "reference": "31f314153020aee5af3537e507fef892ffbf8c85" + "reference": "b59385bb7aa24dae81bcc15850ebecfda7b40838" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/ignition/zipball/31f314153020aee5af3537e507fef892ffbf8c85", - "reference": "31f314153020aee5af3537e507fef892ffbf8c85", + "url": "https://api.github.com/repos/spatie/ignition/zipball/b59385bb7aa24dae81bcc15850ebecfda7b40838", + "reference": "b59385bb7aa24dae81bcc15850ebecfda7b40838", "shasum": "" }, "require": { "ext-json": "*", "ext-mbstring": "*", "php": "^8.0", - "spatie/error-solutions": "^1.0", - "spatie/flare-client-php": "^1.7", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "spatie/backtrace": "^1.7.1", + "spatie/error-solutions": "^1.1.2", + "spatie/flare-client-php": "^1.9", + "symfony/console": "^5.4.42|^6.0|^7.0|^8.0", + "symfony/http-foundation": "^5.4.42|^6.0|^7.0|^8.0", + "symfony/mime": "^5.4.42|^6.0|^7.0|^8.0", + "symfony/var-dumper": "^5.4.42|^6.0|^7.0|^8.0" }, "require-dev": { - "illuminate/cache": "^9.52|^10.0|^11.0|^12.0", + "illuminate/cache": "^9.52|^10.0|^11.0|^12.0|^13.0", "mockery/mockery": "^1.4", - "pestphp/pest": "^1.20|^2.0", + "pestphp/pest": "^1.20|^2.0|^3.0", "phpstan/extension-installer": "^1.1", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-phpunit": "^1.0", "psr/simple-cache-implementation": "*", - "symfony/cache": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", + "symfony/cache": "^5.4.38|^6.0|^7.0|^8.0", + "symfony/process": "^5.4.35|^6.0|^7.0|^8.0", "vlucas/phpdotenv": "^5.5" }, "suggest": { @@ -11129,20 +11089,20 @@ "type": "github" } ], - "time": "2025-02-21T14:31:39+00:00" + "time": "2026-03-17T10:51:08+00:00" }, { "name": "spatie/laravel-ignition", - "version": "2.11.0", + "version": "2.12.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-ignition.git", - "reference": "11f38d1ff7abc583a61c96bf3c1b03610a69cccd" + "reference": "45b3b6e1e73fc161cba2149972698644b99594ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/11f38d1ff7abc583a61c96bf3c1b03610a69cccd", - "reference": "11f38d1ff7abc583a61c96bf3c1b03610a69cccd", + "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/45b3b6e1e73fc161cba2149972698644b99594ee", + "reference": "45b3b6e1e73fc161cba2149972698644b99594ee", "shasum": "" }, "require": { @@ -11152,7 +11112,7 @@ "illuminate/support": "^11.0|^12.0|^13.0", "nesbot/carbon": "^2.72|^3.0", "php": "^8.2", - "spatie/ignition": "^1.15.1", + "spatie/ignition": "^1.16", "symfony/console": "^7.4|^8.0", "symfony/var-dumper": "^7.4|^8.0" }, @@ -11221,20 +11181,20 @@ "type": "github" } ], - "time": "2026-02-22T19:14:05+00:00" + "time": "2026-03-17T12:20:04+00:00" }, { "name": "spatie/laravel-web-tinker", - "version": "1.10.5", + "version": "1.10.6", "source": { "type": "git", "url": "https://github.com/spatie/laravel-web-tinker.git", - "reference": "5eea61d4e63f18f418ca320ec09a35e520fb530d" + "reference": "54ec53ecd275335baaf4b7b7300aeda69e932af6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-web-tinker/zipball/5eea61d4e63f18f418ca320ec09a35e520fb530d", - "reference": "5eea61d4e63f18f418ca320ec09a35e520fb530d", + "url": "https://api.github.com/repos/spatie/laravel-web-tinker/zipball/54ec53ecd275335baaf4b7b7300aeda69e932af6", + "reference": "54ec53ecd275335baaf4b7b7300aeda69e932af6", "shasum": "" }, "require": { @@ -11286,7 +11246,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-web-tinker/issues", - "source": "https://github.com/spatie/laravel-web-tinker/tree/1.10.5" + "source": "https://github.com/spatie/laravel-web-tinker/tree/1.10.6" }, "funding": [ { @@ -11294,7 +11254,7 @@ "type": "custom" } ], - "time": "2026-02-21T21:16:52+00:00" + "time": "2026-04-27T06:48:06+00:00" }, { "name": "staabm/side-effects-detector", @@ -11350,27 +11310,28 @@ }, { "name": "symfony/yaml", - "version": "v8.0.6", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "5f006c50a981e1630bbb70ad409c5d85f9a716e0" + "reference": "efb42bd2c6f4f3ccfd4683583449938b5fc146b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/5f006c50a981e1630bbb70ad409c5d85f9a716e0", - "reference": "5f006c50a981e1630bbb70ad409c5d85f9a716e0", + "url": "https://api.github.com/repos/symfony/yaml/zipball/efb42bd2c6f4f3ccfd4683583449938b5fc146b0", + "reference": "efb42bd2c6f4f3ccfd4683583449938b5fc146b0", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/console": "<7.4" }, "require-dev": { - "symfony/console": "^7.4|^8.0" + "symfony/console": "^7.4|^8.0", + "yaml/yaml-test-suite": "*" }, "bin": [ "Resources/bin/yaml-lint" @@ -11401,7 +11362,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v8.0.6" + "source": "https://github.com/symfony/yaml/tree/v8.1.0" }, "funding": [ { @@ -11421,7 +11382,7 @@ "type": "tidelift" } ], - "time": "2026-02-09T10:14:57+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "theseer/tokenizer", diff --git a/config/app.php b/config/app.php index 0091c22d..61aa3e41 100644 --- a/config/app.php +++ b/config/app.php @@ -173,6 +173,7 @@ App\Providers\TusServiceProvider::class, PHPOpenSourceSaver\JWTAuth\Providers\LaravelServiceProvider::class, App\Providers\BroadcastServiceProvider::class, + App\Providers\OIDCServiceProvider::class, ], /* diff --git a/storage/VERSION b/storage/VERSION index 7e541aec..1565867a 100644 --- a/storage/VERSION +++ b/storage/VERSION @@ -1 +1 @@ -2.2.2 \ No newline at end of file +2.3-dev \ No newline at end of file From ea8fc4ae1a473b6ea1807311521b6f3e54b0dce8 Mon Sep 17 00:00:00 2001 From: dogukanoksuz Date: Thu, 25 Jun 2026 17:47:23 +0300 Subject: [PATCH 02/11] fix: harden authentication endpoints with throttle, strong password policy and timing normalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - throttle: /setup_mfa, /change_password, /reset_password (5/dk), /oidc/callback (30/dk) - StrongPassword rule (10-32 karakter, 1 büyük/küçük/rakam/özel) tüm şifre rotalarında - ProfileController validation sırası düzeltildi (validate -> Hash::make) - Settings/UserController::delete null check (404) - Settings/UserController::update roles.* Rule::exists validation - setup_mfa ve forceChangePassword kullanıcı-yok branch'inde dummy Hash::check ile timing attack engeli --- app/Http/Controllers/API/AuthController.php | 27 +++++++----- .../Controllers/API/ProfileController.php | 14 ++++--- .../API/Settings/UserController.php | 13 +++++- app/Rules/StrongPassword.php | 41 +++++++++++++++++++ routes/api.php | 12 ++++-- 5 files changed, 85 insertions(+), 22 deletions(-) create mode 100644 app/Rules/StrongPassword.php diff --git a/app/Http/Controllers/API/AuthController.php b/app/Http/Controllers/API/AuthController.php index 1aa42744..802db9ff 100644 --- a/app/Http/Controllers/API/AuthController.php +++ b/app/Http/Controllers/API/AuthController.php @@ -9,6 +9,7 @@ use App\Http\Controllers\Controller; use App\Models\SystemSettings; use App\Models\User; +use App\Rules\StrongPassword; use Illuminate\Auth\Events\PasswordReset; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -199,6 +200,12 @@ public function setupTwoFactorAuthentication(Request $request) })->first(); if (! $user) { + // Timing normalization: 'kullanıcı yok' ve 'parola yanlış' sürelerini eşitle. + // Hash::check, "parola yanlış" path'inde çalıştırılan süre (~50ms) kadar zaman alır. + Hash::check( + 'invalid', + '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi' + ); return response()->json(['message' => 'Kullanıcı adı veya şifreniz yanlış.'], 401); } @@ -288,12 +295,7 @@ public function forceChangePassword(Request $request) $validator = Validator::make($request->all(), [ 'email' => 'required|string', 'password' => 'required|string', - 'new_password' => [ - 'string', - 'min:10', - 'max:32', - 'regex:/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[\!\[\]\(\)\{\}\#\?\%\&\*\+\,\-\.\/\:\;\<\=\>\@\^\_\`\~]).{10,}$/', - ], + 'new_password' => ['required', 'string', new StrongPassword], ], [ 'new_password.regex' => 'Yeni parolanız en az 10 karakter uzunluğunda olmalı ve en az 1 sayı, özel karakter ve büyük harf içermelidir.', ]); @@ -309,6 +311,11 @@ public function forceChangePassword(Request $request) ->first(); if (! $user) { + // Timing normalization: 'kullanıcı yok' ve 'parola yanlış' sürelerini eşitle. + Hash::check( + 'invalid', + '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi' + ); return response()->json(['message' => 'Kullanıcı adı veya şifreniz yanlış.'], 401); } @@ -363,11 +370,9 @@ public function resetPassword(Request $request) 'password' => [ 'required', 'string', - 'min:10', - 'max:32', - 'regex:/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[\!\[\]\(\)\{\}\#\?\%\&\*\+\,\-\.\/\:\;\<\=\>\@\^\_\`\~]).{10,}$/', - 'confirmed' - ] + new StrongPassword, + 'confirmed', + ], ]); $status = Password::reset( diff --git a/app/Http/Controllers/API/ProfileController.php b/app/Http/Controllers/API/ProfileController.php index 569bf6bd..3a7f2dc8 100644 --- a/app/Http/Controllers/API/ProfileController.php +++ b/app/Http/Controllers/API/ProfileController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use App\Models\User; +use App\Rules\StrongPassword; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; @@ -29,6 +30,14 @@ public function setInformation(Request $request) ], 403); } + // Validation önce çalışsın; parola değişikliği ancak tüm validationlar geçerse uygulansın. + validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:users,email,' . auth('api')->user()->id, + 'password' => ['nullable', 'string', new StrongPassword], + 'old_password' => ['required_with:password', 'string'], + ]); + $user = User::find(auth('api')->user()->id); if ($request->password) { @@ -48,11 +57,6 @@ public function setInformation(Request $request) ]); } - validate([ - 'name' => 'required|string|max:255', - 'email' => 'required|string|email|max:255|unique:users,email,' . auth('api')->user()->id, - ]); - $session_time = env('JWT_TTL', 120); if ($request->session_time == $session_time) { $session_time = -1; diff --git a/app/Http/Controllers/API/Settings/UserController.php b/app/Http/Controllers/API/Settings/UserController.php index 38a4aa1a..195fad98 100644 --- a/app/Http/Controllers/API/Settings/UserController.php +++ b/app/Http/Controllers/API/Settings/UserController.php @@ -9,6 +9,7 @@ use App\Models\Role; use App\Models\RoleUser; use App\Models\User; +use App\Rules\StrongPassword; use Illuminate\Database\Eloquent\Collection; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; @@ -48,7 +49,7 @@ public function create(Request $request) 'max:255', 'unique:users', ], - 'password' => ['string', 'min:8'], + 'password' => ['nullable', 'string', new StrongPassword], ]); $data = [ @@ -119,8 +120,10 @@ public function update(Request $request) 'max:255', Rule::unique('users')->ignore($user->id), ], - 'password' => ['nullable', 'string', 'min:8'], + 'password' => ['nullable', 'string', new StrongPassword], 'session_time' => ['required', 'integer', 'min:15', 'max:999999'], + 'roles' => ['required', 'array'], + 'roles.*' => [Rule::exists('roles', 'id')], ]); $session_time = env('JWT_TTL', 120); @@ -187,6 +190,12 @@ public function delete(Request $request) { $user = User::where('id', $request->user_id)->first(); + if (! $user) { + return response()->json([ + 'message' => 'Kullanıcı bulunamadı.' + ], 404); + } + // If user type is not local, return error if ($user->auth_type !== 'local') { return response()->json([ diff --git a/app/Rules/StrongPassword.php b/app/Rules/StrongPassword.php new file mode 100644 index 00000000..24b8bf09 --- /dev/null +++ b/app/Rules/StrongPassword.php @@ -0,0 +1,41 @@ +middleware('throttle:login'); - Route::post('/setup_mfa', [AuthController::class, 'setupTwoFactorAuthentication']); + Route::post('/setup_mfa', [AuthController::class, 'setupTwoFactorAuthentication']) + ->middleware('throttle:mfa:5,1'); Route::post('/logout', [AuthController::class, 'logout']); Route::get('/user', [AuthController::class, 'userProfile']); - Route::post('/change_password', [AuthController::class, 'forceChangePassword']); + Route::post('/change_password', [AuthController::class, 'forceChangePassword']) + ->middleware('throttle:password:5,1'); Route::post('/forgot_password', [AuthController::class, 'sendPasswordResetLink']) ->middleware('throttle:forgot-password'); - Route::post('/reset_password', [AuthController::class, 'resetPassword']); + Route::post('/reset_password', [AuthController::class, 'resetPassword']) + ->middleware('throttle:reset:5,1'); Route::get('/oidc/callback', [AuthController::class, 'oidcCallback']) - ->name('oidcCallback'); + ->name('oidcCallback') + ->middleware('throttle:oidc:30,1'); }); Route::post('/notifications/send', [ExternalNotificationController::class, 'accept']); From b8b18db6a9d6f4c9a1ebe2830fad816828363c52 Mon Sep 17 00:00:00 2001 From: dogukanoksuz Date: Thu, 25 Jun 2026 17:47:29 +0300 Subject: [PATCH 03/11] fix: prevent SSRF, zip slip and privileged account abuse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KubernetesController: add_server Liman izni olmayanlara 403 (local cluster kullanımı korunuyor) - Settings/ExtensionController: extractTo öncesi zip entry'lerde path traversal denetimi (Zip Slip) - Helpers::isIpSafeForFetch: link-local (169.254.0.0/16, fe80::/10) engeli, retrieveCertificate chokepoint - Server/UserController: addSudoers/deleteSudoers admin-only, kritik gruplara user ekleme admin-only, audit log, \x20 workaround kaldırıldı (regex validation) --- .../Controllers/API/KubernetesController.php | 13 ++++ .../Controllers/API/Server/UserController.php | 76 ++++++++++++++++++- .../API/Settings/ExtensionController.php | 23 ++++++ app/Http/Helpers.php | 44 +++++++++++ 4 files changed, 152 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/API/KubernetesController.php b/app/Http/Controllers/API/KubernetesController.php index 38e25991..436ae1e9 100644 --- a/app/Http/Controllers/API/KubernetesController.php +++ b/app/Http/Controllers/API/KubernetesController.php @@ -2,12 +2,25 @@ namespace App\Http\Controllers\API; +use App\Exceptions\JsonResponseException; use App\Http\Controllers\Controller; +use App\Models\Permission; use GuzzleHttp\Client; use Illuminate\Http\Request; +use Illuminate\Http\Response; class KubernetesController extends Controller { + public function __construct() + { + $user = auth('api')->user(); + if (! $user || ! Permission::can($user->id, 'liman', 'id', 'add_server')) { + throw new JsonResponseException([ + 'message' => 'Bu işlemi yapmak için yetkiniz yok!', + ], '', Response::HTTP_FORBIDDEN); + } + } + public function getNamespaces(Request $request) { return $this->makeRenderEngineRequest($request, 'namespaces'); diff --git a/app/Http/Controllers/API/Server/UserController.php b/app/Http/Controllers/API/Server/UserController.php index 557c90b5..1a31c67e 100644 --- a/app/Http/Controllers/API/Server/UserController.php +++ b/app/Http/Controllers/API/Server/UserController.php @@ -4,6 +4,7 @@ use App\Exceptions\JsonResponseException; use App\Http\Controllers\Controller; +use App\Models\AuditLog; use App\Models\Permission; use App\System\Command; use Illuminate\Http\Response; @@ -13,6 +14,15 @@ */ class UserController extends Controller { + /** + * Bu listedeki gruplara kullanıcı ekleme admin yetkisi gerektirir. + * wheel/sudo/admin → root; docker/lxd → container escape; disk/shadow → dosya erişimi. + */ + private const PRIVILEGED_GROUPS = [ + 'wheel', 'sudo', 'admin', 'docker', 'lxd', + 'adm', 'shadow', 'disk', 'root', 'kvm', + 'libvirt', 'polkitd', 'systemd-journal', 'kadmin', 'kmem', + ]; public function __construct() { if (! Permission::can(auth('api')->user()->id, 'liman', 'id', 'server_details')) { @@ -213,6 +223,13 @@ public function addLocalGroupUser() { $group = request('group'); $user = request('user'); + + if (! auth('api')->user()->isAdmin() && in_array(strtolower((string) $group), self::PRIVILEGED_GROUPS)) { + throw new JsonResponseException([ + 'message' => 'Kritik gruplara kullanıcı eklemek için yönetici yetkisi gerekir.', + ], '', Response::HTTP_FORBIDDEN); + } + $output = Command::runSudo('usermod -a -G @{:group} @{:user} &> /dev/null && echo 1 || echo 0', [ 'group' => $group, 'user' => $user, @@ -221,6 +238,20 @@ public function addLocalGroupUser() return response()->json('An error occured while adding user to group.', Response::HTTP_UNPROCESSABLE_ENTITY); } + if (in_array(strtolower((string) $group), self::PRIVILEGED_GROUPS)) { + AuditLog::write( + 'server_user', + 'add_to_privileged_group', + [ + 'server_id' => server()->id, + 'server_name' => server()->name, + 'group' => (string) $group, + 'user' => (string) $user, + ], + 'PRIVILEGED_GROUP_USER_ADDED' + ); + } + return response()->json('User added to group successfully.'); } @@ -263,8 +294,17 @@ public function getSudoers() */ public function addSudoers() { + if (! auth('api')->user()->isAdmin()) { + throw new JsonResponseException([ + 'message' => 'Sudoer eklemek için yönetici yetkisi gerekir.', + ], '', Response::HTTP_FORBIDDEN); + } + + validate([ + 'name' => ['required', 'string', 'regex:/^[a-zA-Z0-9._-]+$/'], + ]); + $name = request('name'); - $name = str_replace(' ', '\\x20', (string) $name); $checkFile = Command::runSudo("[ -f '/etc/sudoers.d/{:name}' ] && echo 1 || echo 0", [ 'name' => $name, ]); @@ -281,6 +321,17 @@ public function addSudoers() return response()->json(['message' => 'An error occured while creating sudoer'], Response::HTTP_INTERNAL_SERVER_ERROR); } + AuditLog::write( + 'server_user', + 'add_sudoer', + [ + 'server_id' => server()->id, + 'server_name' => server()->name, + 'sudoer_name' => (string) $name, + ], + 'SUDO_USER_ADDED' + ); + return response()->json('Sudoer created successfully.'); } @@ -293,10 +344,16 @@ public function addSudoers() */ public function deleteSudoers() { + if (! auth('api')->user()->isAdmin()) { + throw new JsonResponseException([ + 'message' => 'Sudoer silmek için yönetici yetkisi gerekir.', + ], '', Response::HTTP_FORBIDDEN); + } + $names = request('names'); - $names = array_map(function ($value) { - return str_replace(' ', '\\x20', (string) $value); - }, $names); + if (! is_array($names)) { + return response()->json(['names' => 'Geçersiz istek.'], Response::HTTP_UNPROCESSABLE_ENTITY); + } foreach ($names as $name) { $output = Command::runSudo( @@ -310,6 +367,17 @@ public function deleteSudoers() } } + AuditLog::write( + 'server_user', + 'delete_sudoers', + [ + 'server_id' => server()->id, + 'server_name' => server()->name, + 'sudoers' => implode(', ', $names), + ], + 'SUDO_USERS_DELETED' + ); + return response()->json('Sudoer deleted successfully.'); } } diff --git a/app/Http/Controllers/API/Settings/ExtensionController.php b/app/Http/Controllers/API/Settings/ExtensionController.php index 7d8f6cd6..4bbed505 100644 --- a/app/Http/Controllers/API/Settings/ExtensionController.php +++ b/app/Http/Controllers/API/Settings/ExtensionController.php @@ -297,6 +297,29 @@ private function setupNewExtension($zipFile) ], 500), null]; } + // Zip Slip protection: validate every entry name before extraction. + // Reject entries that attempt path traversal outside the extraction directory. + for ($i = 0; $i < $zip->numFiles; $i++) { + $stat = $zip->statIndex($i); + if ($stat === false) { + continue; + } + $name = $stat['name']; + if ( + str_contains($name, '..') || + str_starts_with($name, '/') || + str_starts_with($name, '\\') || + str_contains($name, '://') + ) { + return [ + response()->json([ + 'message' => 'Geçersiz eklenti paketi (path traversal tespit edildi): ' . $name, + ], Response::HTTP_UNPROCESSABLE_ENTITY), + null, + ]; + } + } + // Determine a random tmp folder to extract files $path = '/tmp/'.Str::random(); // Extract Zip to the Temp Folder. diff --git a/app/Http/Helpers.php b/app/Http/Helpers.php index 2592168c..5926adeb 100755 --- a/app/Http/Helpers.php +++ b/app/Http/Helpers.php @@ -293,6 +293,46 @@ function knownPorts() } } +if (! function_exists('isIpSafeForFetch')) { + /** + * Check if a hostname/IP is safe for Liman-initiated outbound fetch. + * + * Only link-local addresses are blocked (RFC3927 169.254.0.0/16 for IPv4, + * RFC4291 fe80::/10 for IPv6). These cover AWS/Azure/GCP instance metadata + * services (169.254.169.254) and mDNS. Loopback and RFC1918 private + * addresses are allowed — Liman is regularly used to manage internal + * servers / local Kubernetes clusters. + */ + function isIpSafeForFetch($host): bool + { + if (! is_string($host) || trim($host) === '') { + return false; + } + $host = trim($host); + $ip = filter_var($host, FILTER_VALIDATE_IP) ? $host : gethostbyname($host); + if (! filter_var($ip, FILTER_VALIDATE_IP)) { + return false; + } + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + $parts = explode('.', $ip); + if (count($parts) === 4 && $parts[0] === '169' && $parts[1] === '254') { + return false; + } + } + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + $packed = @inet_pton($ip); + if ($packed !== false && strlen($packed) === 16) { + $firstByte = ord($packed[0]); + $secondByte = ord($packed[1]); + if ($firstByte === 0xfe && ($secondByte & 0xc0) === 0x80) { + return false; + } + } + } + return true; + } +} + if (! function_exists('retrieveCertificate')) { /** * Retrieve certificate content from remote end @@ -302,6 +342,10 @@ function knownPorts() */ function retrieveCertificate($hostname, $port) { + if (! isIpSafeForFetch($hostname)) { + return [false, __('Bu adrese erişim güvenlik nedeniyle engellendi.')]; + } + $get = stream_context_create([ 'ssl' => [ 'capture_peer_cert' => true, From 1d8a888904761b30e8a381fe741204ca3f4dc60e Mon Sep 17 00:00:00 2001 From: dogukanoksuz Date: Thu, 25 Jun 2026 17:47:40 +0300 Subject: [PATCH 04/11] fix: hash search cache key and limit query length MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cache key sha1() ile hash'lendi (sabit uzunluk, binary-safe, injection-proof) - query nullable|string|min:1|max:64 validation - boş query için direkt boş JSON, cache key hesaplama yok --- app/Http/Controllers/API/SearchController.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/API/SearchController.php b/app/Http/Controllers/API/SearchController.php index 4d08d097..60be8eb3 100644 --- a/app/Http/Controllers/API/SearchController.php +++ b/app/Http/Controllers/API/SearchController.php @@ -24,10 +24,20 @@ class SearchController extends Controller */ public function search(Request $request) { + validate([ + 'query' => 'nullable|string|min:1|max:64', + ]); + $searchable = []; - $searchQuery = strtolower($request->input('query')); + $searchQuery = strtolower((string) $request->input('query', '')); + + if (trim($searchQuery) === '') { + return response()->json('{}'); + } + + $cacheKey = 'search:' . auth('api')->user()->id . ':' . sha1($searchQuery); - $results = Cache::remember(auth('api')->user()->id . '_searchable_' . $searchQuery, now()->addHour(), function () use ($searchable, $searchQuery) { + $results = Cache::remember($cacheKey, now()->addHour(), function () use ($searchable, $searchQuery) { $configs = [ 'user' => 'Kullanıcı', 'common' => 'Genel' From 11cfe778b55f51c9d7c438e87b19b41a4ef0fd39 Mon Sep 17 00:00:00 2001 From: dogukanoksuz Date: Wed, 8 Jul 2026 11:14:40 +0000 Subject: [PATCH 05/11] feat: removed all server management features --- .../OIDC/OpenIDConnectClient.php | 14 + app/Http/Controllers/API/SearchController.php | 4 - .../API/Server/DetailsController.php | 286 ------------- .../API/Server/PackageController.php | 68 ---- .../Controllers/API/Server/PortController.php | 64 --- .../API/Server/QueueController.php | 86 ---- .../API/Server/ServiceController.php | 302 -------------- .../API/Server/UpdateController.php | 86 ---- .../Controllers/API/Server/UserController.php | 383 ------------------ .../API/Settings/FeatureFlagController.php | 10 - routes/api.php | 66 +-- 11 files changed, 18 insertions(+), 1351 deletions(-) delete mode 100644 app/Http/Controllers/API/Server/PackageController.php delete mode 100644 app/Http/Controllers/API/Server/PortController.php delete mode 100644 app/Http/Controllers/API/Server/QueueController.php delete mode 100644 app/Http/Controllers/API/Server/ServiceController.php delete mode 100644 app/Http/Controllers/API/Server/UpdateController.php delete mode 100644 app/Http/Controllers/API/Server/UserController.php diff --git a/app/Classes/Authentication/OIDC/OpenIDConnectClient.php b/app/Classes/Authentication/OIDC/OpenIDConnectClient.php index e77b41f1..8a6a00d0 100644 --- a/app/Classes/Authentication/OIDC/OpenIDConnectClient.php +++ b/app/Classes/Authentication/OIDC/OpenIDConnectClient.php @@ -166,6 +166,20 @@ public function buildAuthorizationUrl(string $state, string $nonce): string .http_build_query($params, '', '&'); } + /** + * Jumbojett'in verifyJWTClaims metodu, aud claim string olduğunda ve + * clientID ile eşleşmediğinde in_array'e string haystack geçip TypeError + * fırlatıyor (PHP 8+). aud'yi normalize ederek bu bug'ı düzeltiriz. + */ + protected function verifyJWTClaims($claims, string $accessToken = null): bool + { + if (isset($claims->aud) && !is_array($claims->aud)) { + $claims->aud = [$claims->aud]; + } + + return parent::verifyJWTClaims($claims, $accessToken); + } + /** * Authorization code'u token'a çevir, ID token'ı JWKS/client_secret ile * doğrula ve claim'leri döndür. diff --git a/app/Http/Controllers/API/SearchController.php b/app/Http/Controllers/API/SearchController.php index 60be8eb3..f23c71c9 100644 --- a/app/Http/Controllers/API/SearchController.php +++ b/app/Http/Controllers/API/SearchController.php @@ -31,10 +31,6 @@ public function search(Request $request) $searchable = []; $searchQuery = strtolower((string) $request->input('query', '')); - if (trim($searchQuery) === '') { - return response()->json('{}'); - } - $cacheKey = 'search:' . auth('api')->user()->id . ':' . sha1($searchQuery); $results = Cache::remember($cacheKey, now()->addHour(), function () use ($searchable, $searchQuery) { diff --git a/app/Http/Controllers/API/Server/DetailsController.php b/app/Http/Controllers/API/Server/DetailsController.php index c3239f76..a4384c7f 100644 --- a/app/Http/Controllers/API/Server/DetailsController.php +++ b/app/Http/Controllers/API/Server/DetailsController.php @@ -2,16 +2,11 @@ namespace App\Http\Controllers\API\Server; -use App\Exceptions\JsonResponseException; use App\Http\Controllers\Controller; use App\Models\Permission; use App\Models\Server; -use App\System\Command; -use Carbon\Carbon; -use GuzzleHttp\Exception\GuzzleException; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Http\Response; /** * Server Details Controller @@ -40,218 +35,6 @@ public function index() ->values(); } - /** - * Get single server detail - * - * @return JsonResponse|Response - * @throws GuzzleException - */ - public function server() - { - $server = server(); - if (! $server) { - throw new JsonResponseException([ - 'message' => 'Sunucu bulunamadı.' - ], Response::HTTP_NOT_FOUND); - } - - if (! Permission::can(auth('api')->user()->id, 'liman', 'id', 'server_details')) { - throw new JsonResponseException([ - 'message' => 'Bu işlemi yapmak için yetkiniz yok!' - ], '', Response::HTTP_FORBIDDEN); - } - - try { - if ($server->isWindows()) { - preg_match('/\d+/', (string) $server->getUptime(), $output); - $uptime = $output[0]; - } else { - $uptime = $server->getUptime(); - } - $uptime = Carbon::parse($uptime)->diffForHumans(); - } catch (\Throwable) { - $uptime = __('Uptime parse edemiyorum.'); - } - - $outputs = [ - 'hostname' => $server->getHostname(), - 'os' => $server->getVersion(), - 'services' => $server->getNoOfServices(), - 'processes' => $server->getNoOfProcesses(), - 'uptime' => $uptime, - ]; - - if ($server->canRunCommand()) { - $outputs['user'] = Command::run('whoami'); - } - - $server['is_favorite'] = $server->isFavorite(); - - return response()->json([ - 'server' => $server, - 'details' => $outputs, - ]); - } - - /** - * Get stats of server for graphs - * - * @return array - * @throws GuzzleException - */ - public function stats() - { - if (! Permission::can(auth('api')->user()->id, 'liman', 'id', 'server_details')) { - throw new JsonResponseException([ - 'message' => 'Bu işlemi yapmak için yetkiniz yok!' - ], '', Response::HTTP_FORBIDDEN); - } - - if (server()->isLinux()) { - $cores = str_replace("cpu cores\t: ", '', trim(explode("\n", Command::runSudo("cat /proc/cpuinfo | grep 'cpu cores'"))[0])); - - $cpuPercent = Command::runSudo( - "ps -eo %cpu --no-headers | grep -v 0.0 | awk '{s+=$1} END {print s/NR*10}'" - ); - $ramPercent = Command::runSudo( - "free | grep Mem | awk '{print $3/$2 * 100.0}'" - ); - $ioPercent = (float) Command::runSudo( - "iostat -d | tail -n +4 | head -n -1 | awk '{s+=$2} END {print s}'" - ); - $firstDown = $this->calculateNetworkBytes(); - $firstUp = $this->calculateNetworkBytes(false); - sleep(1); - $secondDown = $this->calculateNetworkBytes(); - $secondUp = $this->calculateNetworkBytes(false); - - return [ - 'cpu' => round((float) $cpuPercent / $cores, 2), - 'ram' => round((float) $ramPercent, 2), - 'io' => round((float) $ioPercent, 2), - 'network' => [ - 'download' => round(($secondDown - $firstDown) / 1024 / 2, 2), - 'upload' => round(($secondUp - $firstUp) / 1024 / 2, 2), - ], - ]; - } - - return [ - 'disk' => 0, - 'ram' => 0, - 'cpu' => 0, - 'network' => [ - 'download' => 0, - 'upload' => 0, - ], - ]; - } - - /** - * Get server specs - * - * @return JsonResponse - * @throws GuzzleException - */ - public function specs() - { - if (! Permission::can(auth('api')->user()->id, 'liman', 'id', 'server_details')) { - throw new JsonResponseException([ - 'message' => 'Bu işlemi yapmak için yetkiniz yok!' - ], '', Response::HTTP_FORBIDDEN); - } - - $cores = str_replace("cpu cores\t: ", '', trim(explode("\n", Command::runSudo("cat /proc/cpuinfo | grep 'cpu cores'"))[0])); - $cpu = str_replace("model name\t: ", '', trim(explode("\n", Command::runSudo("cat /proc/cpuinfo | grep 'model name'"))[0])); - $ram = Command::runSudo('free -m | grep ^Mem | tr -s " " | cut -f2 -d" "'); - if ($ram > 1000) { - $ram = round($ram / 1000, 1).' GB'; - } else { - $ram = $ram.' GB'; - } - $model = Command::runSudo('dmidecode -s system-product-name'); - $manufacturer = Command::runSudo('dmidecode -s system-manufacturer'); - - return response()->json([ - 'cpu' => $cores.'x '.$cpu, - 'ram' => $ram, - 'model' => $model, - 'manufacturer' => $manufacturer, - ]); - } - - /** - * Top CPU using processes - * - * @return Application|Factory|View - * - * @throws GuzzleException - */ - public function topCpuProcesses() - { - if (! Permission::can(auth('api')->user()->id, 'liman', 'id', 'server_details')) { - throw new JsonResponseException([ - 'message' => 'Bu işlemi yapmak için yetkiniz yok!' - ], '', Response::HTTP_FORBIDDEN); - } - - $output = trim( - Command::runSudo( - "ps -eo pid,%cpu,user,cmd --sort=-%cpu --no-headers | head -n 5 | awk '{print $1\"*-*\"$2\"*-*\"$3\"*-*\"$4}'" - ) - ); - - return response()->json($this->parsePsOutput($output)); - } - - /** - * Get top memory processes - * - * @return Application|Factory|View - * - * @throws GuzzleException - */ - public function topMemoryProcesses() - { - if (! Permission::can(auth('api')->user()->id, 'liman', 'id', 'server_details')) { - throw new JsonResponseException([ - 'message' => 'Bu işlemi yapmak için yetkiniz yok!' - ], '', Response::HTTP_FORBIDDEN); - } - - $output = trim( - Command::runSudo( - "ps -eo pid,%mem,user,cmd --sort=-%mem --no-headers | head -n 5 | awk '{print $1\"*-*\"$2\"*-*\"$3\"*-*\"$4}'" - ) - ); - - return response()->json($this->parsePsOutput($output)); - } - - /** - * Top disk usage - * - * @return Application|Factory|View - * - * @throws GuzzleException - */ - public function topDiskUsage() - { - if (! Permission::can(auth('api')->user()->id, 'liman', 'id', 'server_details')) { - throw new JsonResponseException([ - 'message' => 'Bu işlemi yapmak için yetkiniz yok!' - ], '', Response::HTTP_FORBIDDEN); - } - - $output = trim( - Command::runSudo( - "df --output=pcent,source,size,used -hl -x squashfs -x tmpfs -x devtmpfs | sed -n '1!p' | head -n 5 | sort -hr | awk '{print $1\"*-*\"$2\"*-*\"$3\"*-*\"$4}'" - ) - ); - - return response()->json($this->parseDfOutput($output)); - } - /** * Add server to favorites * @@ -268,73 +51,4 @@ public function favorite(Request $request) 'message' => 'İşlem başarılı.' ]); } - - /** - * Calculate network flow as bytes - * - * @return int - * - * @throws GuzzleException - */ - private function calculateNetworkBytes($download = true) - { - $text = $download ? 'rx_bytes' : 'tx_bytes'; - $count = 0; - $raw = Command::runSudo('cat /sys/class/net/*/statistics/:text:', [ - 'text' => $text, - ]); - foreach (explode("\n", trim((string) $raw)) as $data) { - $count += intval($data); - } - - return $count; - } - - /** - * Parse ps-aux output - * - * @return array - */ - private function parsePsOutput($output) - { - $cores = str_replace("cpu cores\t: ", '', trim(explode("\n", Command::runSudo("cat /proc/cpuinfo | grep 'cpu cores'"))[0])); - - $data = []; - foreach (explode("\n", (string) $output) as $row) { - $row = explode('*-*', $row); - $row[3] = str_replace('\\', '/', $row[3]); - $fetch = explode('/', $row[3]); - $data[] = [ - 'pid' => $row[0], - 'percent' => round($row[1] / $cores, 1), - 'user' => $row[2], - 'cmd' => end($fetch), - ]; - } - - return $data; - } - - /** - * Parse df-h output - * - * @return array - */ - private function parseDfOutput($output) - { - $data = []; - foreach (explode("\n", (string) $output) as $row) { - $row = explode('*-*', $row); - $row[1] = str_replace('\\', '/', $row[1]); - $fetch = explode('/', $row[1]); - $data[] = [ - 'percent' => str_replace('%', '', $row[0]), - 'source' => end($fetch), - 'size' => $row[2], - 'used' => $row[3], - ]; - } - - return $data; - } } diff --git a/app/Http/Controllers/API/Server/PackageController.php b/app/Http/Controllers/API/Server/PackageController.php deleted file mode 100644 index d8100fed..00000000 --- a/app/Http/Controllers/API/Server/PackageController.php +++ /dev/null @@ -1,68 +0,0 @@ -user()->id, 'liman', 'id', 'server_details')) { - throw new JsonResponseException([ - 'message' => 'Bu işlemi yapmak için yetkiniz yok!' - ], '', Response::HTTP_FORBIDDEN); - } - } - - /** - * List packages that is installed on system - * - * @return JsonResponse - * @throws GuzzleException - */ - public function index() - { - $pkgman = Command::runSudo( - 'which apt >/dev/null 2>&1 && echo apt || echo rpm' - ); - - if ($pkgman == 'apt') { - $raw = Command::runSudo( - "apt list --installed 2>/dev/null | sed '1,1d'" - ); - } else { - $raw = Command::runSudo( - "yum list --installed 2>/dev/null | awk {'print $1 \" \" $2 \" \" $3'} | sed '1,1d'" - ); - } - - $packages = []; - foreach (explode("\n", $raw) as $package) { - if ($package == '') { - continue; - } - $row = explode(' ', $package); - try { - $packages[] = [ - 'name' => $row[0], - 'version' => $row[1], - 'type' => $row[2], - ]; - } catch (Exception) { - } - } - - return response()->json($packages); - } -} diff --git a/app/Http/Controllers/API/Server/PortController.php b/app/Http/Controllers/API/Server/PortController.php deleted file mode 100644 index 4a31b184..00000000 --- a/app/Http/Controllers/API/Server/PortController.php +++ /dev/null @@ -1,64 +0,0 @@ -user()->id, 'liman', 'id', 'server_details')) { - throw new JsonResponseException([ - 'message' => 'Bu işlemi yapmak için yetkiniz yok!' - ], '', Response::HTTP_FORBIDDEN); - } - } - - /** - * Get open ports on server - * - * @return JsonResponse|Response - * - * @throws GuzzleException - * @throws GuzzleException - */ - public function index() - { - if (server()->os != 'linux') { - return respond('Bu sunucuda portları kontrol edemezsiniz!', Response::HTTP_FORBIDDEN); - } - - $output = trim( - Command::runSudo( - "lsof -i -P -n | grep -v '\-'| awk -F' ' '{print $1,$3,$5,$8,$9}' | sed 1,1d" - ) - ); - - if (empty($output)) { - return response()->json([]); - } - - $arr = []; - foreach (explode("\n", $output) as $line) { - $row = explode(' ', $line); - $arr[] = [ - 'name' => $row[0], - 'username' => $row[1], - 'ip_type' => $row[2], - 'packet_type' => $row[3], - 'port' => $row[4], - ]; - } - - return response()->json($arr); - } -} diff --git a/app/Http/Controllers/API/Server/QueueController.php b/app/Http/Controllers/API/Server/QueueController.php deleted file mode 100644 index 591d27ab..00000000 --- a/app/Http/Controllers/API/Server/QueueController.php +++ /dev/null @@ -1,86 +0,0 @@ -user()->id, 'liman', 'id', 'server_details')) { - throw new JsonResponseException([ - 'message' => 'Bu işlemi yapmak için yetkiniz yok!' - ], '', Response::HTTP_FORBIDDEN); - } - } - - public function index(Request $request) - { - $queues = Queue::where('type', 'install') - ->whereJsonContains('data->server_id', $request->server_id) - ->orderBy('updated_at', 'desc') - ->get(); - - return $queues; - } - - public function create(Request $request) - { - $tus = app('tus-server'); - $file = $tus->getCache()->get($request->file); - - if ($file['size'] > 1024 * 1024 * 1024 * 2) { - // Delete file from filesystem - unlink($file['file_path']); - - return response()->json([ - 'file' => "Dosya boyutu 2GB'dan büyük olamaz.", - ], Response::HTTP_UNPROCESSABLE_ENTITY); - } - - if (! in_array(pathinfo($file['file_path'], PATHINFO_EXTENSION), ['deb', 'rpm'])) { - // Delete file from filesystem - unlink($file['file_path']); - - return response()->json([ - 'file' => 'Sadece .deb ve .rpm uzantılı dosyalar yüklenebilir.', - ], Response::HTTP_UNPROCESSABLE_ENTITY); - } - - $client = new Client([ - 'verify' => false, - ]); - try { - $res = $client->request('POST', env('RENDER_ENGINE_ADDRESS', 'https://127.0.0.1:2806').'/queue', [ - 'json' => [ - 'type' => 'install', - 'data' => [ - 'server_id' => $request->server_id, - 'path' => $file['file_path'], - ], - ], - 'timeout' => 30, - 'cookies' => convertToCookieJar(request(), '127.0.0.1'), - ]); - $output = (string) $res->getBody(); - - $isJson = isJson($output, true); - if ($isJson) { - return response()->json($isJson, $res->getStatusCode()); - } else { - return response()->json( - $output, $res->getStatusCode() - ); - } - } catch (\Exception $e) { - return response()->json([ - 'message' => __('Liman render service is not working or crashed. ').! env('APP_DEBUG', false) ?: $e->getMessage(), - ], Response::HTTP_GATEWAY_TIMEOUT); - } - } -} diff --git a/app/Http/Controllers/API/Server/ServiceController.php b/app/Http/Controllers/API/Server/ServiceController.php deleted file mode 100644 index 0b185e6a..00000000 --- a/app/Http/Controllers/API/Server/ServiceController.php +++ /dev/null @@ -1,302 +0,0 @@ -user()->id, 'liman', 'id', 'server_services')) { - throw new JsonResponseException([ - 'message' => 'Bu işlemi yapmak için yetkiniz yok!' - ], '', Response::HTTP_FORBIDDEN); - } - } - - /** - * Service List - * - * @return JsonResponse - * @throws GuzzleException - */ - public function index() - { - $services = []; - if (server()->isLinux()) { - $raw = Command::runSudo( - "systemctl list-units --all | grep service | awk '{print $1 \":\"$2\" \"$3\" \"$4\":\"$5\" \"$6\" \"$7\" \"$8\" \"$9\" \"$10}'", - false - ); - foreach (explode("\n", $raw) as &$package) { - if ($package == '') { - continue; - } - if (str_contains($package, '●')) { - $package = explode('●:', $package)[1]; - } - $row = explode(':', trim($package)); - try { - if (str_contains($row[0], 'sysusers.service')) { - continue; - } - - $status = explode(' ', $row[1]); - $services[] = [ - 'name' => strlen($row[0]) > 50 ? substr($row[0], 0, 50) . '...' : $row[0], - 'description' => strlen($row[2]) > 60 ? substr($row[2], 0, 60) . '...' : $row[2], - 'status' => [ - 'loaded' => $status[0] == 'loaded', - 'active' => $status[1] == 'active', - 'running' => $status[2], - ], - ]; - } catch (Exception) { - } - } - - $raw = Command::runSudo( - "systemctl list-unit-files --state=disabled | grep service | awk '{print $1 \":\"$2}'", - false - ); - - foreach (explode("\n", $raw) as &$package) { - if ($package == '') { - continue; - } - $row = explode(':', trim($package)); - $services[] = [ - 'name' => strlen($row[0]) > 50 ? substr($row[0], 0, 50) . '...' : $row[0], - 'status' => $row[1] == 'disabled', - ]; - } - } else { - $rawServices = Command::run( - "(Get-WmiObject win32_service | select Name, DisplayName, State, StartMode) -replace '\s\s+',':'" - ); - $services = []; - foreach (explode('}', $rawServices) as $service) { - $row = explode(';', substr($service, 2)); - if ($row[0] == '') { - continue; - } - try { - $services[] = [ - 'name' => trim(explode('=', $row[0])[1]), - 'description' => trim(explode('=', $row[1])[1]), - 'status' => trim(explode('=', $row[2])[1]), - ]; - } catch (Exception) { - } - } - } - - return response()->json($services); - } - - /** - * Start service - * - * @param Request $request - * @return JsonResponse - * @throws GuzzleException - */ - public function start(Request $request) - { - validate([ - 'services' => 'required', - ]); - - $services = request('services'); - if (server()->isLinux()) { - foreach ($services as $service) { - Command::runSudo('systemctl start @{:service}', [ - 'service' => $service, - ]); - } - } else { - foreach ($services as $service) { - Command::run('net start @{:service}', [ - 'service' => $service, - ]); - } - } - - return response()->json([ - 'message' => 'Servis başlatıldı.' - ]); - } - - /** - * Stop service - * - * @param Request $request - * @return JsonResponse - * @throws GuzzleException - */ - public function stop(Request $request) - { - validate([ - 'services' => 'required', - ]); - - $services = request('services'); - if (server()->isLinux()) { - foreach ($services as $service) { - Command::runSudo('systemctl stop @{:service}', [ - 'service' => $service, - ]); - } - } else { - foreach ($services as $service) { - Command::run('net stop @{:service}', [ - 'service' => $service, - ]); - } - } - - return response()->json([ - 'message' => 'Servis durduruldu.' - ]); - } - - /** - * Restart service - * - * @param Request $request - * @return JsonResponse - * @throws GuzzleException - */ - public function restart(Request $request) - { - validate([ - 'services' => 'required', - ]); - - $services = request('services'); - if (server()->isLinux()) { - foreach ($services as $service) { - Command::runSudo('systemctl restart @{:service}', [ - 'service' => $service, - ]); - } - } else { - foreach ($services as $service) { - Command::run('net stop @{:service}', [ - 'service' => $service, - ]); - Command::run('net start @{:service}', [ - 'service' => $service, - ]); - } - } - - return response()->json([ - 'message' => 'Servis yeniden başlatıldı.' - ]); - } - - /** - * Enable service - * - * @param Request $request - * @return JsonResponse - * @throws GuzzleException - * @throws JsonResponseException - */ - public function enable(Request $request) - { - validate([ - 'services' => 'required', - ]); - - $services = request('services'); - if (server()->isLinux()) { - foreach ($services as $service) { - Command::runSudo('systemctl enable @{:service}', [ - 'service' => $service, - ]); - } - } else { - foreach ($services as $service) { - Command::run('sc config @{:service} start=auto', [ - 'service' => $service, - ]); - } - } - - return response()->json([ - 'message' => 'Servis devreye alındı.' - ]); - } - - /** - * Disable service - * - * @param Request $request - * @return JsonResponse - * @throws GuzzleException - */ - public function disable(Request $request) - { - validate([ - 'services' => 'required', - ]); - - $services = request('services'); - if (server()->isLinux()) { - foreach ($services as $service) { - Command::runSudo('systemctl disable @{:service}', [ - 'service' => $service, - ]); - } - } else { - foreach ($services as $service) { - Command::run('sc config @{:service} start=demand', [ - 'service' => $service, - ]); - } - } - - return response()->json([ - 'message' => 'Servis devreden çıkarıldı.' - ]); - } - - /** - * Service status - * - * @param Request $request - * @return JsonResponse - * @throws GuzzleException - */ - public function status(Request $request) - { - $request->validate([ - 'service_name' => 'required', - ]); - - $service = request('service_name'); - if (server()->isLinux()) { - return response()->json(Command::runSudo('systemctl status @{:service}', [ - 'service' => $service, - ])); - } - - return response()->json(Command::run('sc query @{:service}', [ - 'service' => $service, - ])); - } -} diff --git a/app/Http/Controllers/API/Server/UpdateController.php b/app/Http/Controllers/API/Server/UpdateController.php deleted file mode 100644 index bfd13ba5..00000000 --- a/app/Http/Controllers/API/Server/UpdateController.php +++ /dev/null @@ -1,86 +0,0 @@ -user()->id, 'liman', 'id', 'server_details')) { - throw new JsonResponseException([ - 'message' => 'Bu işlemi yapmak için yetkiniz yok!' - ], '', Response::HTTP_FORBIDDEN); - } - } - - /** - * Updatable packages list - * - * @return JsonResponse|void - * @throws GuzzleException - */ - public function index() - { - $pkgman = Command::runSudo( - 'which apt >/dev/null 2>&1 && echo apt || echo rpm' - ); - - $updates = []; - if ($pkgman == 'apt') { - $raw = Command::runSudo( - 'apt-get -qq update 2> /dev/null > /dev/null; '. - "apt list --upgradable 2>/dev/null | sed '1,1d'" - ); - foreach (explode("\n", $raw) as $package) { - if ($package == '' || str_contains($package, 'List')) { - continue; - } - $row = explode(' ', $package, 4); - try { - $updates[] = [ - 'name' => $row[0], - 'version' => $row[1], - 'type' => $row[2], - 'status' => $row[3], - ]; - } catch (\Exception) { - } - } - - return response()->json($updates); - } - - if ($pkgman == 'rpm') { - $raw = Command::runSudo( - "yum list upgrades --exclude=*.src 2>/dev/null | awk {'print $1 \" \" $2 \" \" $3'} | sed '1,3d'" - ); - foreach (explode("\n", $raw) as $package) { - if ($package == '' || str_contains($package, 'List')) { - continue; - } - $row = explode(' ', $package, 4); - try { - $updates[] = [ - 'name' => $row[0], - 'version' => $row[1], - 'type' => $row[2], - ]; - } catch (\Exception) { - } - } - - return response()->json($updates); - } - } -} diff --git a/app/Http/Controllers/API/Server/UserController.php b/app/Http/Controllers/API/Server/UserController.php deleted file mode 100644 index 1a31c67e..00000000 --- a/app/Http/Controllers/API/Server/UserController.php +++ /dev/null @@ -1,383 +0,0 @@ -user()->id, 'liman', 'id', 'server_details')) { - throw new JsonResponseException([ - 'message' => 'Bu işlemi yapmak için yetkiniz yok!' - ], '', Response::HTTP_FORBIDDEN); - } - } - - /** - * Get local users on system - * - * @return JsonResponse|Response - * - * @throws GuzzleException - * @throws GuzzleException - */ - public function getLocalUsers() - { - $users = []; - if (server()->isLinux()) { - $output = Command::runSudo( - "cut -d: -f1,3 /etc/passwd | egrep ':[0-9]{4}$' | cut -d: -f1" - ); - $output = trim($output); - if (empty($output)) { - $users = []; - } else { - $output = explode("\n", $output); - foreach ($output as $user) { - $users[] = [ - 'user' => $user, - ]; - } - } - } - - if (server()->isWindows() && server()->canRunCommand()) { - $output = Command::run( - 'Get-LocalUser | Where { $_.Enabled -eq $True} | Select-Object Name' - ); - $output = trim($output); - if (empty($output)) { - $users = []; - } else { - $output = explode("\r\n", $output); - foreach ($output as $key => $user) { - if ($key == 0 || $key == 1) { - continue; - } - $users[] = [ - 'user' => $user, - ]; - } - } - } - - return response()->json($users); - } - - /** - * Create local user on server - * - * @return JsonResponse|Response - * - * @throws GuzzleException - */ - public function addLocalUser() - { - $user_name = request('username'); - $user_password = request('password'); - $user_password_confirmation = request('password_confirmation'); - if ($user_password !== $user_password_confirmation) { - return response()->json([ - 'password' => 'Şifreler eşleşmiyor.' - ], Response::HTTP_UNPROCESSABLE_ENTITY); - } - $output = Command::runSudo('useradd --no-user-group -p $(openssl passwd -1 {:user_password}) {:user_name} -s "/bin/bash" &> /dev/null && echo 1 || echo 0', [ - 'user_password' => $user_password, - 'user_name' => $user_name, - ]); - if ($output == '0') { - return response()->json([ - 'message' => 'Kullanıcı oluşturulurken hata oluştu.' - ], Response::HTTP_INTERNAL_SERVER_ERROR); - } - - return response()->json([ - 'message' => 'Kullanıcı başarıyla oluşturuldu.' - ]); - } - - /** - * Get local groups - * - * @return JsonResponse|Response - * - * @throws GuzzleException - */ - public function getLocalGroups() - { - $groups = []; - if (server()->isLinux()) { - $output = Command::runSudo("getent group | cut -d ':' -f1"); - $output = trim($output); - if (empty($output)) { - $groups = []; - } else { - $output = explode("\n", $output); - foreach ($output as $group) { - $groups[] = [ - 'group' => $group, - ]; - } - $groups = array_reverse($groups); - } - } - - if (server()->isWindows() && server()->canRunCommand()) { - $output = Command::run( - 'Get-LocalGroup | Select-Object Name' - ); - $output = trim($output); - if (empty($output)) { - $groups = []; - } else { - $output = explode("\r\n", $output); - foreach ($output as $key => $group) { - if ($key == 0 || $key == 1) { - continue; - } - $groups[] = [ - 'group' => $group, - ]; - } - } - } - - return response()->json($groups); - } - - /** - * Get local group details - * - * @return JsonResponse|Response - * - * @throws GuzzleException - */ - public function getLocalGroupDetails() - { - $group = request('group'); - $output = Command::runSudo("getent group @{:group} | cut -d ':' -f4", [ - 'group' => $group, - ]); - - $users = []; - if (! empty($output)) { - $users = array_map(function ($value) { - return ['name' => $value]; - }, explode(',', (string) $output)); - } - - return response()->json($users); - } - - /** - * Create local group on server - * - * @return JsonResponse|Response - * - * @throws GuzzleException - */ - public function addLocalGroup() - { - $group_name = request('group_name'); - $output = Command::runSudo('groupadd @{:group_name} &> /dev/null && echo 1 || echo 0', [ - 'group_name' => $group_name, - ]); - if ($output == '0') { - return response()->json([ - 'message' => 'Grup oluşturulurken hata oluştu.' - ], Response::HTTP_INTERNAL_SERVER_ERROR); - } - - return response()->json([ - 'message' => 'Grup başarıyla oluşturuldu.' - ], 200); - } - - /** - * Add user to group - * - * @return JsonResponse|Response - * - * @throws GuzzleException - */ - public function addLocalGroupUser() - { - $group = request('group'); - $user = request('user'); - - if (! auth('api')->user()->isAdmin() && in_array(strtolower((string) $group), self::PRIVILEGED_GROUPS)) { - throw new JsonResponseException([ - 'message' => 'Kritik gruplara kullanıcı eklemek için yönetici yetkisi gerekir.', - ], '', Response::HTTP_FORBIDDEN); - } - - $output = Command::runSudo('usermod -a -G @{:group} @{:user} &> /dev/null && echo 1 || echo 0', [ - 'group' => $group, - 'user' => $user, - ]); - if ($output != '1') { - return response()->json('An error occured while adding user to group.', Response::HTTP_UNPROCESSABLE_ENTITY); - } - - if (in_array(strtolower((string) $group), self::PRIVILEGED_GROUPS)) { - AuditLog::write( - 'server_user', - 'add_to_privileged_group', - [ - 'server_id' => server()->id, - 'server_name' => server()->name, - 'group' => (string) $group, - 'user' => (string) $user, - ], - 'PRIVILEGED_GROUP_USER_ADDED' - ); - } - - return response()->json('User added to group successfully.'); - } - - /** - * Get sudoers list - * - * @return JsonResponse|Response - * - * @throws GuzzleException - * @throws GuzzleException - */ - public function getSudoers() - { - $command = <<<'EOT' - sh -c "cat /etc/sudoers /etc/sudoers.d/* | grep -v '^#\|^Defaults' | sed '/^$/d'" - EOT; - - $output = Command::runSudo($command); - - $sudoers = []; - if (! empty($output)) { - $sudoers = array_map(function ($value) { - $val = strtr($value, "\t\n\r ", ' '); - $fetch = explode(' ', $val); - $name = array_shift($fetch); - - return ['name' => $name, 'access' => implode(' ', $fetch)]; - }, explode("\n", $output)); - } - - return response()->json($sudoers); - } - - /** - * Create sudoer on server - * - * @return JsonResponse|Response - * - * @throws GuzzleException - */ - public function addSudoers() - { - if (! auth('api')->user()->isAdmin()) { - throw new JsonResponseException([ - 'message' => 'Sudoer eklemek için yönetici yetkisi gerekir.', - ], '', Response::HTTP_FORBIDDEN); - } - - validate([ - 'name' => ['required', 'string', 'regex:/^[a-zA-Z0-9._-]+$/'], - ]); - - $name = request('name'); - $checkFile = Command::runSudo("[ -f '/etc/sudoers.d/{:name}' ] && echo 1 || echo 0", [ - 'name' => $name, - ]); - if ($checkFile == '1') { - return response()->json(['name' => 'Bu isimde başka bir kullanıcı mevcut.'], Response::HTTP_UNPROCESSABLE_ENTITY); - } - $output = Command::runSudo( - 'echo "{:name} ALL=(ALL:ALL) ALL" | ' . sudo() . ' tee /etc/sudoers.d/{:name} &> /dev/null && echo 1 || echo 0', - [ - 'name' => $name, - ] - ); - if ($output == '0') { - return response()->json(['message' => 'An error occured while creating sudoer'], Response::HTTP_INTERNAL_SERVER_ERROR); - } - - AuditLog::write( - 'server_user', - 'add_sudoer', - [ - 'server_id' => server()->id, - 'server_name' => server()->name, - 'sudoer_name' => (string) $name, - ], - 'SUDO_USER_ADDED' - ); - - return response()->json('Sudoer created successfully.'); - } - - /** - * Delete sudoer - * - * @return JsonResponse|Response - * - * @throws GuzzleException - */ - public function deleteSudoers() - { - if (! auth('api')->user()->isAdmin()) { - throw new JsonResponseException([ - 'message' => 'Sudoer silmek için yönetici yetkisi gerekir.', - ], '', Response::HTTP_FORBIDDEN); - } - - $names = request('names'); - if (! is_array($names)) { - return response()->json(['names' => 'Geçersiz istek.'], Response::HTTP_UNPROCESSABLE_ENTITY); - } - - foreach ($names as $name) { - $output = Command::runSudo( - 'bash -c "if [ -f \"/etc/sudoers.d/{:name}\" ]; then rm /etc/sudoers.d/{:name} && echo 1 || echo 0; else echo 0; fi"', - [ - 'name' => $name, - ] - ); - if ($output == '0') { - return response()->json('An error occured while deleting sudoer.', Response::HTTP_INTERNAL_SERVER_ERROR); - } - } - - AuditLog::write( - 'server_user', - 'delete_sudoers', - [ - 'server_id' => server()->id, - 'server_name' => server()->name, - 'sudoers' => implode(', ', $names), - ], - 'SUDO_USERS_DELETED' - ); - - return response()->json('Sudoer deleted successfully.'); - } -} diff --git a/app/Http/Controllers/API/Settings/FeatureFlagController.php b/app/Http/Controllers/API/Settings/FeatureFlagController.php index ba092b8a..efa7616a 100644 --- a/app/Http/Controllers/API/Settings/FeatureFlagController.php +++ b/app/Http/Controllers/API/Settings/FeatureFlagController.php @@ -24,11 +24,6 @@ class FeatureFlagController extends Controller 'settings_external_notifications' => true, 'settings_subscriptions' => true, 'settings_health' => true, - 'server_services' => true, - 'server_packages' => true, - 'server_updates' => true, - 'server_user_management' => true, - 'server_open_ports' => true, 'server_access_logs' => true, 'dashboard_most_used_extensions' => true, 'dashboard_favorite_servers' => true, @@ -83,11 +78,6 @@ public function saveConfiguration(Request $request) 'settings_external_notifications' => 'present|boolean', 'settings_subscriptions' => 'present|boolean', 'settings_health' => 'present|boolean', - 'server_services' => 'present|boolean', - 'server_packages' => 'present|boolean', - 'server_updates' => 'present|boolean', - 'server_user_management' => 'present|boolean', - 'server_open_ports' => 'present|boolean', 'server_access_logs' => 'present|boolean', 'dashboard_most_used_extensions' => 'present|boolean', 'dashboard_favorite_servers' => 'present|boolean', diff --git a/routes/api.php b/routes/api.php index dce4707e..8698f7cd 100644 --- a/routes/api.php +++ b/routes/api.php @@ -12,10 +12,9 @@ use App\Http\Controllers\API\Server; use App\Http\Controllers\API\ServerController; use App\Http\Controllers\API\Settings; -use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; -Route::get('/', function (Request $request) { +Route::get('/', function () { return response()->json([ 'version' => getVersion(), ]); @@ -30,7 +29,7 @@ Route::post('/login', [AuthController::class, 'login']) ->middleware('throttle:login'); Route::post('/setup_mfa', [AuthController::class, 'setupTwoFactorAuthentication']) - ->middleware('throttle:mfa:5,1'); + ->middleware('throttle:5,1'); Route::post('/logout', [AuthController::class, 'logout']); Route::get('/user', [AuthController::class, 'userProfile']); Route::post('/change_password', [AuthController::class, 'forceChangePassword']) @@ -38,10 +37,10 @@ Route::post('/forgot_password', [AuthController::class, 'sendPasswordResetLink']) ->middleware('throttle:forgot-password'); Route::post('/reset_password', [AuthController::class, 'resetPassword']) - ->middleware('throttle:reset:5,1'); + ->middleware('throttle:5,1'); Route::get('/oidc/callback', [AuthController::class, 'oidcCallback']) ->name('oidcCallback') - ->middleware('throttle:oidc:30,1'); + ->middleware('throttle:30,1'); }); Route::post('/notifications/send', [ExternalNotificationController::class, 'accept']); @@ -109,19 +108,10 @@ Route::group(['prefix' => '{server_id}', 'middleware' => ['server']], function () { Route::get('/', [Server\DetailsController::class, 'server']); - Route::get('/specs', [Server\DetailsController::class, 'specs']); // Kubernetes Route::get('/kubernetes_deployment_details', [KubernetesController::class, 'getDeploymentDetailsFromServer']); - // Stats - Route::group(['prefix' => 'stats'], function () { - Route::get('/', [Server\DetailsController::class, 'stats']); - Route::get('/cpu', [Server\DetailsController::class, 'topCpuProcesses']); - Route::get('/ram', [Server\DetailsController::class, 'topMemoryProcesses']); - Route::get('/disk', [Server\DetailsController::class, 'topDiskUsage']); - }); - // Extensions Route::group(['prefix' => 'extensions'], function () { // Extension List That Assigned To Server @@ -138,59 +128,11 @@ ->middleware(['extension']); }); - // Services - Route::group(['prefix' => 'services'], function () { - Route::get('/', [Server\ServiceController::class, 'index']); - Route::post('/status', [Server\ServiceController::class, 'status']); - Route::post('/start', [Server\ServiceController::class, 'start']); - Route::post('/stop', [Server\ServiceController::class, 'stop']); - Route::post('/restart', [Server\ServiceController::class, 'restart']); - Route::post('/enable', [Server\ServiceController::class, 'enable']); - Route::post('/disable', [Server\ServiceController::class, 'disable']); - }); - - // Packages - Route::group(['prefix' => 'packages'], function () { - Route::get('/', [Server\PackageController::class, 'index']); - - // Queue - Route::group(['prefix' => 'queue'], function () { - Route::get('/', [Server\QueueController::class, 'index']); - Route::post('/', [Server\QueueController::class, 'create']); - }); - }); - - // Updates - Route::group(['prefix' => 'updates'], function () { - Route::get('/', [Server\UpdateController::class, 'index']); - }); - // Access Logs Route::group(['prefix' => 'access_logs'], function () { Route::get('/', [Server\AccessLogController::class, 'index']); Route::get('/{log_id}', [Server\AccessLogController::class, 'details']); }); - - // Ports - Route::group(['prefix' => 'ports'], function () { - Route::get('/', [Server\PortController::class, 'index']); - }); - - // Users - Route::group(['prefix' => 'users'], function () { - Route::get('/local', [Server\UserController::class, 'getLocalUsers']); - Route::post('/local', [Server\UserController::class, 'addLocalUser']); - - Route::get('/groups', [Server\UserController::class, 'getLocalGroups']); - Route::post('/groups', [Server\UserController::class, 'addLocalGroup']); - Route::get('/groups/users', [Server\UserController::class, 'getLocalGroupDetails']); - Route::post('/groups/users', [Server\UserController::class, 'addLocalGroupUser']); - - Route::get('/sudoers', [Server\UserController::class, 'getSudoers']); - Route::post('/sudoers', [Server\UserController::class, 'addSudoers']); - Route::delete('/sudoers', [Server\UserController::class, 'deleteSudoers']); - }); - }); }); From f8e5ce86facba6dbdea72e002481a857df93dbcf Mon Sep 17 00:00:00 2001 From: dogukanoksuz Date: Fri, 10 Jul 2026 06:49:15 +0000 Subject: [PATCH 06/11] fix: rate limiter issue on password reset --- routes/api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/api.php b/routes/api.php index 8698f7cd..79c046e1 100644 --- a/routes/api.php +++ b/routes/api.php @@ -33,7 +33,7 @@ Route::post('/logout', [AuthController::class, 'logout']); Route::get('/user', [AuthController::class, 'userProfile']); Route::post('/change_password', [AuthController::class, 'forceChangePassword']) - ->middleware('throttle:password:5,1'); + ->middleware('throttle:5,1'); Route::post('/forgot_password', [AuthController::class, 'sendPasswordResetLink']) ->middleware('throttle:forgot-password'); Route::post('/reset_password', [AuthController::class, 'resetPassword']) From bc1fc729d34cde873aaf64ebe77bca2d96e00e49 Mon Sep 17 00:00:00 2001 From: dogukanoksuz Date: Fri, 10 Jul 2026 06:52:46 +0000 Subject: [PATCH 07/11] fix: oidc account takeover logic --- .../OIDC/OIDCUserProvisioner.php | 64 ++++++++++++------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/app/Classes/Authentication/OIDC/OIDCUserProvisioner.php b/app/Classes/Authentication/OIDC/OIDCUserProvisioner.php index 3dcac10e..2bb24c4e 100644 --- a/app/Classes/Authentication/OIDC/OIDCUserProvisioner.php +++ b/app/Classes/Authentication/OIDC/OIDCUserProvisioner.php @@ -23,40 +23,42 @@ public function findOrCreate(object $claims): ?User try { $user = User::where('oidc_sub', $userInfo['sub'])->first(); - if (! $user) { - if (trim($userInfo['email']) === '') { - throw new \Exception('User creation failed, email value is required'); - } - - $user = User::where('email', $userInfo['email'])->first(); - - if ($user) { - $user->update([ - 'oidc_sub' => $userInfo['sub'], - 'auth_type' => 'oidc', - 'name' => $userInfo['display_name'], - ]); - } else { - $user = $this->createUser($userInfo); - } - } else { + if ($user) { $user->update([ 'name' => $userInfo['display_name'], 'email' => $userInfo['email'], - 'auth_type' => 'oidc', ]); + + return $this->finalize($user); } - if (! $user->getJWTIdentifier()) { - Log::error('User JWT identifier is null', [ - 'user_id' => $user->id, - 'user_exists' => $user->exists, + if (trim($userInfo['email']) === '') { + throw new \Exception('User creation failed, email value is required'); + } + + $existing = User::where('email', $userInfo['email'])->first(); + + if ($existing) { + if ($existing->auth_type !== 'oidc') { + Log::error('OIDC provisioning refused: email is already in use by a non-OIDC account', [ + 'sub' => $userInfo['sub'], + 'email' => $userInfo['email'], + 'existing_user_id' => $existing->id, + 'existing_auth_type' => $existing->auth_type, + ]); + + return null; + } + + $existing->update([ + 'oidc_sub' => $userInfo['sub'], + 'name' => $userInfo['display_name'], ]); - return null; + return $this->finalize($existing); } - return $user; + return $this->finalize($this->createUser($userInfo)); } catch (\Exception $e) { Log::error('User creation/update failed: '.$e->getMessage()); @@ -64,6 +66,20 @@ public function findOrCreate(object $claims): ?User } } + private function finalize(User $user): ?User + { + if (! $user->getJWTIdentifier()) { + Log::error('User JWT identifier is null', [ + 'user_id' => $user->id, + 'user_exists' => $user->exists, + ]); + + return null; + } + + return $user; + } + /** * @return array{sub: string, email: string, display_name: string, username: string, external_token: ?string} */ From 1c62180070377635e2ced9d826186550e24e29e2 Mon Sep 17 00:00:00 2001 From: dogukanoksuz Date: Fri, 10 Jul 2026 07:12:17 +0000 Subject: [PATCH 08/11] fix: public upload endpoint --- app/Http/Kernel.php | 79 +++++++++----- app/Http/Middleware/TusAuthenticated.php | 20 ++-- app/Http/Middleware/VerifyExtensionAccess.php | 100 ++++++++++++++++++ app/Providers/RouteServiceProvider.php | 4 + routes/web.php | 48 ++++----- 5 files changed, 188 insertions(+), 63 deletions(-) create mode 100644 app/Http/Middleware/VerifyExtensionAccess.php diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index deeb743f..77b6a937 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -2,8 +2,30 @@ namespace App\Http; +use App\Http\Middleware\Admin; +use App\Http\Middleware\Authenticate; +use App\Http\Middleware\BlockExceptLimans; +use App\Http\Middleware\Extension; +use App\Http\Middleware\ForcePasswordChange; +use App\Http\Middleware\PermissionManager; +use App\Http\Middleware\Server; +use App\Http\Middleware\VerifyExtensionAccess; +use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; +use Illuminate\Auth\Middleware\Authorize; +use Illuminate\Auth\Middleware\EnsureEmailIsVerified; +use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Illuminate\Foundation\Http\Kernel as HttpKernel; +use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull; +use Illuminate\Foundation\Http\Middleware\ValidatePostSize; use Illuminate\Http\Middleware\HandleCors; +use Illuminate\Http\Middleware\SetCacheHeaders; +use Illuminate\Routing\Middleware\SubstituteBindings; +use Illuminate\Routing\Middleware\ThrottleRequests; +use Illuminate\Routing\Middleware\ValidateSignature; +use Illuminate\Session\Middleware\AuthenticateSession; +use Illuminate\Session\Middleware\StartSession; +use Illuminate\View\Middleware\ShareErrorsFromSession; +use PragmaRX\Google2FALaravel\MiddlewareStateless; /** * Kernel @@ -16,9 +38,9 @@ class Kernel extends HttpKernel HandleCors::class, Middleware\XssSanitization::class, Middleware\CheckForMaintenanceMode::class, - \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, + ValidatePostSize::class, Middleware\TrimStrings::class, - \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, + ConvertEmptyStringsToNull::class, Middleware\TrustProxies::class, Middleware\EncryptCookies::class, Middleware\APILogin::class, @@ -26,12 +48,12 @@ class Kernel extends HttpKernel protected $middlewareGroups = [ 'web' => [ - \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, - \Illuminate\Session\Middleware\StartSession::class, - \Illuminate\Session\Middleware\AuthenticateSession::class, - \Illuminate\View\Middleware\ShareErrorsFromSession::class, - \Illuminate\Routing\Middleware\SubstituteBindings::class, - \App\Http\Middleware\ForcePasswordChange::class, + AddQueuedCookiesToResponse::class, + StartSession::class, + AuthenticateSession::class, + ShareErrorsFromSession::class, + SubstituteBindings::class, + ForcePasswordChange::class, ], 'api' => [ @@ -44,28 +66,29 @@ class Kernel extends HttpKernel ]; protected $middlewareAliases = [ - 'auth' => \App\Http\Middleware\Authenticate::class, - 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, - 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, - 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, - 'can' => \Illuminate\Auth\Middleware\Authorize::class, - 'server' => \App\Http\Middleware\Server::class, - 'permissions' => \App\Http\Middleware\PermissionManager::class, - 'admin' => \App\Http\Middleware\Admin::class, - 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, - 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, - 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, - 'extension' => \App\Http\Middleware\Extension::class, - 'block_except_limans' => \App\Http\Middleware\BlockExceptLimans::class, - 'google2fa' => \PragmaRX\Google2FALaravel\MiddlewareStateless::class, + 'auth' => Authenticate::class, + 'auth.basic' => AuthenticateWithBasicAuth::class, + 'bindings' => SubstituteBindings::class, + 'cache.headers' => SetCacheHeaders::class, + 'can' => Authorize::class, + 'server' => Server::class, + 'permissions' => PermissionManager::class, + 'admin' => Admin::class, + 'signed' => ValidateSignature::class, + 'throttle' => ThrottleRequests::class, + 'verified' => EnsureEmailIsVerified::class, + 'extension' => Extension::class, + 'extension.access' => VerifyExtensionAccess::class, + 'block_except_limans' => BlockExceptLimans::class, + 'google2fa' => MiddlewareStateless::class, ]; protected $middlewarePriority = [ - \Illuminate\Session\Middleware\StartSession::class, - \Illuminate\View\Middleware\ShareErrorsFromSession::class, - \App\Http\Middleware\Authenticate::class, - \Illuminate\Session\Middleware\AuthenticateSession::class, - \Illuminate\Routing\Middleware\SubstituteBindings::class, - \Illuminate\Auth\Middleware\Authorize::class, + StartSession::class, + ShareErrorsFromSession::class, + Authenticate::class, + AuthenticateSession::class, + SubstituteBindings::class, + Authorize::class, ]; } diff --git a/app/Http/Middleware/TusAuthenticated.php b/app/Http/Middleware/TusAuthenticated.php index 68eebf2e..a2aa37ea 100644 --- a/app/Http/Middleware/TusAuthenticated.php +++ b/app/Http/Middleware/TusAuthenticated.php @@ -17,8 +17,6 @@ class TusAuthenticated implements TusMiddleware { /** - * @param Request $request - * @param Response $response * @return void */ public function handle(Request $request, Response $response) @@ -26,6 +24,7 @@ public function handle(Request $request, Response $response) // 1. Check if already authenticated via JWT Authorization header if (auth('api')->check()) { $this->checkExtensionPermission(); + return; } @@ -33,23 +32,25 @@ public function handle(Request $request, Response $response) if (auth('web')->check()) { auth('api')->login(auth('web')->user()); $this->checkExtensionPermission(); + return; } // 3. Try Extension-Token (JWT from sandbox customRequestData['token']) - $token = ""; + $token = ''; if (request()->token) { $token = request()->token; - } else if (request()->headers->get('Extension-Token')) { + } elseif (request()->headers->get('Extension-Token')) { $token = request()->headers->get('Extension-Token'); } if (! $token) { // 4. Try cookie-based JWT (web middleware group doesn't run CookieJWTAuthenticator) if (request()->hasCookie('token')) { - request()->headers->set('Authorization', 'Bearer ' . request()->cookie('token')); + request()->headers->set('Authorization', 'Bearer '.request()->cookie('token')); if (auth('api')->check()) { $this->checkExtensionPermission(); + return; } } @@ -59,7 +60,7 @@ public function handle(Request $request, Response $response) // Validate the JWT token try { - request()->headers->set('Authorization', 'Bearer ' . $token); + request()->headers->set('Authorization', 'Bearer '.$token); if (! auth('api')->check()) { throw new UnauthorizedHttpException('', 'Invalid Extension-Token.'); } @@ -69,7 +70,8 @@ public function handle(Request $request, Response $response) $this->checkExtensionPermission(); - Log::info('Extension-Token validated for user ' . auth('api')->user()->id . '. IP: ' . request()->ip()); + Log::info('Extension-Token validated for user '.auth('api')->user()->id.'. IP: '.request()->ip()); + return true; } @@ -81,12 +83,12 @@ private function checkExtensionPermission(): void $extensionId = request()->headers->get('extension-id'); if (! $extensionId) { - return; + throw new UnauthorizedHttpException('', 'Extension-Id header is missing.'); } $user = auth('api')->user(); if (! $user) { - return; + throw new UnauthorizedHttpException('', 'Authenticated user not found.'); } if (! Permission::can($user->id, 'extension', 'id', $extensionId)) { diff --git a/app/Http/Middleware/VerifyExtensionAccess.php b/app/Http/Middleware/VerifyExtensionAccess.php new file mode 100644 index 00000000..d241f669 --- /dev/null +++ b/app/Http/Middleware/VerifyExtensionAccess.php @@ -0,0 +1,100 @@ +resolveUser($request); + + if (! $user) { + throw new HttpException(401, 'Yetkisiz erişim.'); + } + + $extensionId = $request->headers->get('extension-id') + ?: $request->input('extension_id'); + + if (! $extensionId) { + throw new HttpException(400, 'Eklenti belirtilmedi.'); + } + + $extension = Extension::find($extensionId); + + if (! $extension) { + throw new HttpException(404, 'Eklenti bulunamadı.'); + } + + if (! Permission::can($user->id, 'extension', 'id', $extensionId)) { + throw new HttpException(403, 'Bu işlem için yetkiniz bulunmamaktadır.'); + } + + $request->attributes->set('extension', $extension); + + return $next($request); + } + + /** + * Resolve the authenticated user from any supported token source. + * + * @return User|null + */ + private function resolveUser(Request $request) + { + if (auth('api')->check()) { + return auth('api')->user(); + } + + if (auth('web')->check()) { + auth('api')->login(auth('web')->user()); + + return auth('api')->user(); + } + + $token = $request->input('token') + ?: $request->headers->get('Extension-Token'); + + if ($token) { + $request->headers->set('Authorization', 'Bearer '.$token); + if (auth('api')->check()) { + return auth('api')->user(); + } + } + + if ($request->hasCookie('token')) { + $request->headers->set( + 'Authorization', + 'Bearer '.$request->cookie('token') + ); + if (auth('api')->check()) { + return auth('api')->user(); + } + } + + return null; + } +} diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index eeab429c..73d00e16 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -38,6 +38,10 @@ public function boot() return Limit::perMinutes(5, 2)->by($request->email.$request->ip()); }); + RateLimiter::for('upload', function ($request) { + return Limit::perMinute(60)->by($request->user('api')?->id ?: $request->ip()); + }); + parent::boot(); Route::middleware([]) diff --git a/routes/web.php b/routes/web.php index 0ee064f1..3249e40a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -13,29 +13,21 @@ require_once app_path('Http/Controllers/Extension/Sandbox/_routes.php'); Route::any('/upload/{any?}', function () { - $server = app('tus-server'); - $extension_id = request()->headers->get('extension-id'); - $extension = \App\Models\Extension::find($extension_id); - if ($extension) { - $path = '/liman/extensions/'.strtolower((string) $extension->name); - } else { - $path = storage_path(); - } + $extension = request()->attributes->get('extension'); + $path = '/liman/extensions/'.strtolower((string) $extension->name); if (! file_exists($path.'/uploads')) { mkdir($path.'/uploads'); - if ($extension) { - rootSystem()->fixExtensionPermissions($extension_id, $extension->name); - } else { - rootSystem()->fixExtensionPermissions('liman', 'liman'); - } + rootSystem()->fixExtensionPermissions($extension->id, $extension->name); } + $server = app('tus-server'); $server->setUploadDir($path.'/uploads'); $response = $server->serve(); return $response->send(); }) - ->where('any', '.*'); + ->where('any', '.*') + ->middleware(['extension.access', 'throttle:upload']); Route::post('/upload_info', function () { request()->validate([ @@ -44,20 +36,24 @@ $key = request('key'); $server = app('tus-server'); $info = $server->getCache()->get($key); - $extension_id = request('extension_id'); - $extension = \App\Models\Extension::find($extension_id); - if ($extension_id) { - $extension_path = explode('/uploads/', (string) $info['file_path'], 2)[0]; - $info['file_path'] = str_replace( - $extension_path, - '', - (string) $info['file_path'] - ); - rootSystem()->fixExtensionPermissions($extension_id, $extension->name); + + if (! $info) { + return response()->json([ + 'message' => 'Dosya bulunamadı.', + ], 404); } - return $info; -}); + $extension = request()->attributes->get('extension'); + + rootSystem()->fixExtensionPermissions($extension->id, $extension->name); + + return [ + 'name' => $info['name'] ?? basename((string) ($info['file_path'] ?? '')), + 'size' => $info['size'] ?? 0, + 'offset' => $info['offset'] ?? 0, + 'file_path' => '/uploads/'.basename((string) ($info['file_path'] ?? '')), + ]; +})->middleware(['extension.access', 'throttle:upload']); Route::get( '/eklenti/{extension_id}/public/{any}', From 2df4b4743808dc636ecffce184c2989936fcdf02 Mon Sep 17 00:00:00 2001 From: dogukanoksuz Date: Fri, 10 Jul 2026 07:17:22 +0000 Subject: [PATCH 09/11] fix: external notification rate limit --- .../API/ExternalNotificationController.php | 36 ++++++++----------- app/Providers/RouteServiceProvider.php | 4 +++ routes/api.php | 5 +-- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/app/Http/Controllers/API/ExternalNotificationController.php b/app/Http/Controllers/API/ExternalNotificationController.php index d9eb9b77..52b98fa5 100644 --- a/app/Http/Controllers/API/ExternalNotificationController.php +++ b/app/Http/Controllers/API/ExternalNotificationController.php @@ -19,8 +19,6 @@ class ExternalNotificationController extends Controller /** * Accepts external notifications from outside * - * @param Request $request - * @return JsonResponse * @throws JsonResponseException */ public function accept(Request $request): JsonResponse @@ -28,17 +26,11 @@ public function accept(Request $request): JsonResponse $channel = ExternalNotification::where('token', $request->token) ->first(); - // If token not found, return 404 error - if (! $channel) { + // Return the same error for both invalid token and IP mismatch + // to prevent a differential 404/403 token enumeration oracle. + if (! $channel || ! ip_in_range($request->ip(), $channel->ip)) { return response()->json([ - 'message' => 'token is missing' - ], 404); - } - - // If IP not in range, return 403 error - if (! ip_in_range($request->ip(), $channel->ip)) { - return response()->json([ - 'message' => 'ip is not in range' + 'message' => 'Yetkisiz erişim.', ], 403); } @@ -46,21 +38,21 @@ public function accept(Request $request): JsonResponse // If level is information and success, change request data to trivial if ($request->level === 'information' || $request->level === 'success') { $request->merge([ - 'level' => 'trivial' + 'level' => 'trivial', ]); } // If level is warning, change request data to medium if ($request->level === 'warning') { $request->merge([ - 'level' => 'medium' + 'level' => 'medium', ]); } // If level is error, change request data to high if ($request->level === 'error') { $request->merge([ - 'level' => 'critical' + 'level' => 'critical', ]); } } @@ -69,25 +61,27 @@ public function accept(Request $request): JsonResponse 'title' => 'required', 'content' => 'required', 'level' => 'required|in:critical,high,medium,low,trivial', + 'send_to' => 'nullable|in:all,admins,non_admins', + 'mail' => 'nullable|boolean', ]); $notification = Notification::send( $request->level, - "CUSTOM", + 'CUSTOM', [ - "title" => $request->title, - "content" => $request->content, + 'title' => $request->title, + 'content' => $request->content, ], - $request->send_to, + $request->send_to ?? 'all', (bool) $request->mail ); $channel->update([ - 'last_used' => now() + 'last_used' => now(), ]); return response()->json([ - 'notification' => $notification + 'notification' => $notification, ], (bool) $notification ? 200 : 500); } } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 73d00e16..8fd1b5b3 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -42,6 +42,10 @@ public function boot() return Limit::perMinute(60)->by($request->user('api')?->id ?: $request->ip()); }); + RateLimiter::for('external-notifications', function ($request) { + return Limit::perMinute(10)->by($request->ip()); + }); + parent::boot(); Route::middleware([]) diff --git a/routes/api.php b/routes/api.php index 79c046e1..2633181f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -43,7 +43,8 @@ ->middleware('throttle:30,1'); }); -Route::post('/notifications/send', [ExternalNotificationController::class, 'accept']); +Route::post('/notifications/send', [ExternalNotificationController::class, 'accept']) + ->middleware('throttle:external-notifications'); // Protected Routes Route::group(['middleware' => ['auth:api', 'permissions']], function () { @@ -100,7 +101,7 @@ Route::patch('/{server_id}', [ServerController::class, 'update']); Route::delete('/{server_id}', [ServerController::class, 'delete']); Route::post('/{server_id}/favorites', [Server\DetailsController::class, 'favorite']); - + // Server Creation Validations Route::post('/check_access', [ServerController::class, 'checkAccess']); Route::post('/check_connection', [ServerController::class, 'checkConnection']); From c9f1739bb28325af640e4c07a5e3f9daca5056cc Mon Sep 17 00:00:00 2001 From: dogukanoksuz Date: Fri, 10 Jul 2026 07:20:11 +0000 Subject: [PATCH 10/11] chore: packages updated --- composer.lock | 353 ++++++++++++++++++++++++-------------------------- 1 file changed, 168 insertions(+), 185 deletions(-) diff --git a/composer.lock b/composer.lock index c3748be4..cb4b6063 100644 --- a/composer.lock +++ b/composer.lock @@ -1267,22 +1267,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.12.3", + "version": "7.14.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "9aa17bcdd777ee31df9fc83c337ca4ca2340def3" + "reference": "aef242412e13128b5049864867bb49fc37dd39de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9aa17bcdd777ee31df9fc83c337ca4ca2340def3", - "reference": "9aa17bcdd777ee31df9fc83c337ca4ca2340def3", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/aef242412e13128b5049864867bb49fc37dd39de", + "reference": "aef242412e13128b5049864867bb49fc37dd39de", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.5", - "guzzlehttp/psr7": "^2.12.3", + "guzzlehttp/promises": "^2.5.1", + "guzzlehttp/psr7": "^2.12.4", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.5 || ^3.0", @@ -1294,8 +1294,8 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "guzzle/client-integration-tests": "3.0.2", - "guzzlehttp/test-server": "^0.5.1", + "guzzle/client-integration-tests": "3.0.3", + "guzzlehttp/test-server": "^0.6", "php-http/message-factory": "^1.1", "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" @@ -1375,7 +1375,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.12.3" + "source": "https://github.com/guzzle/guzzle/tree/7.14.0" }, "funding": [ { @@ -1391,20 +1391,20 @@ "type": "tidelift" } ], - "time": "2026-06-23T15:29:02+00:00" + "time": "2026-07-08T22:54:09+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.5.0", + "version": "2.5.1", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "4360e982f87f5f258bf872d094647791db2f4c8e" + "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e", - "reference": "4360e982f87f5f258bf872d094647791db2f4c8e", + "url": "https://api.github.com/repos/guzzle/promises/zipball/9ad1e4fc607446a055b95870c7f668e93b5cff29", + "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29", "shasum": "" }, "require": { @@ -1459,7 +1459,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.5.0" + "source": "https://github.com/guzzle/promises/tree/2.5.1" }, "funding": [ { @@ -1475,20 +1475,20 @@ "type": "tidelift" } ], - "time": "2026-06-02T12:23:43+00:00" + "time": "2026-07-08T15:48:39+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.12.3", + "version": "2.12.4", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d" + "reference": "51e27f9e2b332ab3e72f4520d5ff4f3c68c3577c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/7ec62dc3f44aa218487dbed81a9bf9bc647be55d", - "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/51e27f9e2b332ab3e72f4520d5ff4f3c68c3577c", + "reference": "51e27f9e2b332ab3e72f4520d5ff4f3c68c3577c", "shasum": "" }, "require": { @@ -1578,7 +1578,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.12.3" + "source": "https://github.com/guzzle/psr7/tree/2.12.4" }, "funding": [ { @@ -1594,20 +1594,20 @@ "type": "tidelift" } ], - "time": "2026-06-23T15:21:08+00:00" + "time": "2026-07-08T15:56:20+00:00" }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.8", + "version": "v1.0.9", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "9c19128923b05a5d7355e5d2318d7808b7e33bbd" + "reference": "d7580af6d3f8384325d9cd3e99b21c3ed1848176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/9c19128923b05a5d7355e5d2318d7808b7e33bbd", - "reference": "9c19128923b05a5d7355e5d2318d7808b7e33bbd", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/d7580af6d3f8384325d9cd3e99b21c3ed1848176", + "reference": "d7580af6d3f8384325d9cd3e99b21c3ed1848176", "shasum": "" }, "require": { @@ -1664,7 +1664,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.8" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.9" }, "funding": [ { @@ -1680,7 +1680,7 @@ "type": "tidelift" } ], - "time": "2026-06-23T13:02:23+00:00" + "time": "2026-07-08T16:19:22+00:00" }, { "name": "jumbojett/openid-connect-php", @@ -1726,16 +1726,16 @@ }, { "name": "laravel/framework", - "version": "v12.62.0", + "version": "v12.63.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "f7e61eb1e0e06a38996802b769bce9127aec227c" + "reference": "7adfddbf4738f2e6cae5419b0e6bc46d4cccfbcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/f7e61eb1e0e06a38996802b769bce9127aec227c", - "reference": "f7e61eb1e0e06a38996802b769bce9127aec227c", + "url": "https://api.github.com/repos/laravel/framework/zipball/7adfddbf4738f2e6cae5419b0e6bc46d4cccfbcf", + "reference": "7adfddbf4738f2e6cae5419b0e6bc46d4cccfbcf", "shasum": "" }, "require": { @@ -1944,7 +1944,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-06-09T13:50:13+00:00" + "time": "2026-07-07T14:16:35+00:00" }, { "name": "laravel/helpers", @@ -2005,16 +2005,16 @@ }, { "name": "laravel/prompts", - "version": "v0.3.20", + "version": "v0.3.21", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "6d53c826dfd33ec5f4ac91f816ff327727c7988b" + "reference": "7753c65c281c2550c7c183f14e18062073b7d821" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/6d53c826dfd33ec5f4ac91f816ff327727c7988b", - "reference": "6d53c826dfd33ec5f4ac91f816ff327727c7988b", + "url": "https://api.github.com/repos/laravel/prompts/zipball/7753c65c281c2550c7c183f14e18062073b7d821", + "reference": "7753c65c281c2550c7c183f14e18062073b7d821", "shasum": "" }, "require": { @@ -2058,9 +2058,9 @@ "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.20" + "source": "https://github.com/laravel/prompts/tree/v0.3.21" }, - "time": "2026-06-24T19:16:38+00:00" + "time": "2026-06-26T00:11:25+00:00" }, { "name": "laravel/reverb", @@ -2686,16 +2686,16 @@ }, { "name": "league/flysystem", - "version": "3.35.1", + "version": "3.35.2", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "f23af6c5aafd958a7593029a271d77baf5ed793c" + "reference": "b277b5dc3d56650b68904117124e79c851e12376" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f23af6c5aafd958a7593029a271d77baf5ed793c", - "reference": "f23af6c5aafd958a7593029a271d77baf5ed793c", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/b277b5dc3d56650b68904117124e79c851e12376", + "reference": "b277b5dc3d56650b68904117124e79c851e12376", "shasum": "" }, "require": { @@ -2763,9 +2763,9 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.35.1" + "source": "https://github.com/thephpleague/flysystem/tree/3.35.2" }, - "time": "2026-06-25T06:52:23+00:00" + "time": "2026-07-06T14:42:07+00:00" }, { "name": "league/flysystem-local", @@ -2818,16 +2818,16 @@ }, { "name": "league/mime-type-detection", - "version": "1.16.0", + "version": "1.17.0", "source": { "type": "git", "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", - "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/f5f47eff7c48ed1003069a2ca67f316fb4021c76", + "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76", "shasum": "" }, "require": { @@ -2837,7 +2837,7 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", "phpstan/phpstan": "^0.12.68", - "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0 || ^11.0 || ^12.0" }, "type": "library", "autoload": { @@ -2858,7 +2858,7 @@ "description": "Mime-type detection for Flysystem", "support": { "issues": "https://github.com/thephpleague/mime-type-detection/issues", - "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.17.0" }, "funding": [ { @@ -2870,7 +2870,7 @@ "type": "tidelift" } ], - "time": "2024-09-21T08:32:55+00:00" + "time": "2026-07-09T11:49:27+00:00" }, { "name": "league/oauth2-client", @@ -3332,16 +3332,16 @@ }, { "name": "nesbot/carbon", - "version": "3.13.0", + "version": "3.13.1", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "40f6618f052df16b545f626fbf9a878e6497d16a" + "reference": "2937ad3d1d2c506fd2bc97d571438a95641f44e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/40f6618f052df16b545f626fbf9a878e6497d16a", - "reference": "40f6618f052df16b545f626fbf9a878e6497d16a", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/2937ad3d1d2c506fd2bc97d571438a95641f44e2", + "reference": "2937ad3d1d2c506fd2bc97d571438a95641f44e2", "shasum": "" }, "require": { @@ -3433,7 +3433,7 @@ "type": "tidelift" } ], - "time": "2026-06-18T13:49:15+00:00" + "time": "2026-07-09T18:23:49+00:00" }, { "name": "nette/schema", @@ -3595,20 +3595,19 @@ }, { "name": "nikic/php-parser", - "version": "v5.7.0", + "version": "v5.8.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", "shasum": "" }, "require": { - "ext-ctype": "*", "ext-json": "*", "ext-tokenizer": "*", "php": ">=7.4" @@ -3647,9 +3646,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" }, - "time": "2025-12-06T11:56:16+00:00" + "time": "2026-07-04T14:30:18+00:00" }, { "name": "nunomaduro/termwind", @@ -4855,16 +4854,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.23", + "version": "v0.12.24", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4" + "reference": "ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4dcc0f08047d52bbde475eda481146fd8e27e1a4", - "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1", + "reference": "ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1", "shasum": "" }, "require": { @@ -4928,9 +4927,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.23" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.24" }, - "time": "2026-05-23T13:41:31+00:00" + "time": "2026-06-29T15:41:09+00:00" }, { "name": "pusher/pusher-php-server", @@ -5978,16 +5977,16 @@ }, { "name": "symfony/console", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" + "reference": "92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", - "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", + "url": "https://api.github.com/repos/symfony/console/zipball/92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87", + "reference": "92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87", "shasum": "" }, "require": { @@ -6052,7 +6051,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.13" + "source": "https://github.com/symfony/console/tree/v7.4.14" }, "funding": [ { @@ -6072,7 +6071,7 @@ "type": "tidelift" } ], - "time": "2026-05-24T08:56:14+00:00" + "time": "2026-06-16T11:50:14+00:00" }, { "name": "symfony/css-selector", @@ -6145,16 +6144,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", "shasum": "" }, "require": { @@ -6192,7 +6191,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" }, "funding": [ { @@ -6212,20 +6211,20 @@ "type": "tidelift" } ], - "time": "2026-04-13T15:52:40+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/error-handler", - "version": "v7.4.8", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa" + "reference": "4e1a093b481f323e6e326451f9760c3868430673" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", - "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/4e1a093b481f323e6e326451f9760c3868430673", + "reference": "4e1a093b481f323e6e326451f9760c3868430673", "shasum": "" }, "require": { @@ -6274,7 +6273,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.4.8" + "source": "https://github.com/symfony/error-handler/tree/v7.4.14" }, "funding": [ { @@ -6294,20 +6293,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-06-05T06:22:21+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.4.9", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101" + "reference": "51fe3d170227be8d1772214b82ae506e15ed78ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e4a2e29753c7801f7a8340e066cfa788f3bc8101", - "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/51fe3d170227be8d1772214b82ae506e15ed78ff", + "reference": "51fe3d170227be8d1772214b82ae506e15ed78ff", "shasum": "" }, "require": { @@ -6359,7 +6358,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.9" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.14" }, "funding": [ { @@ -6379,20 +6378,20 @@ "type": "tidelift" } ], - "time": "2026-04-18T13:18:21+00:00" + "time": "2026-06-06T11:10:32+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", - "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", "shasum": "" }, "require": { @@ -6439,7 +6438,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1" }, "funding": [ { @@ -6459,20 +6458,20 @@ "type": "tidelift" } ], - "time": "2026-01-05T13:30:16+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/finder", - "version": "v7.4.8", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "e0be088d22278583a82da281886e8c3592fbf149" + "reference": "13b38720174286f55d1761152b575a8d1436fc25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", - "reference": "e0be088d22278583a82da281886e8c3592fbf149", + "url": "https://api.github.com/repos/symfony/finder/zipball/13b38720174286f55d1761152b575a8d1436fc25", + "reference": "13b38720174286f55d1761152b575a8d1436fc25", "shasum": "" }, "require": { @@ -6507,7 +6506,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.8" + "source": "https://github.com/symfony/finder/tree/v7.4.14" }, "funding": [ { @@ -6527,20 +6526,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-06-27T08:31:18+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "bc354f47c62301e990b7874fa662326368508e2c" + "reference": "06db5ae1552177bf8572f8908839f12e3c06aed3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bc354f47c62301e990b7874fa662326368508e2c", - "reference": "bc354f47c62301e990b7874fa662326368508e2c", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/06db5ae1552177bf8572f8908839f12e3c06aed3", + "reference": "06db5ae1552177bf8572f8908839f12e3c06aed3", "shasum": "" }, "require": { @@ -6589,7 +6588,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.4.13" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.14" }, "funding": [ { @@ -6609,20 +6608,20 @@ "type": "tidelift" } ], - "time": "2026-05-24T11:20:33+00:00" + "time": "2026-06-11T07:31:44+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "9df847980c436451f4f51d1284491bb4356dd989" + "reference": "e99af79b1e776646eda0e1c23b7b45c184ff99be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9df847980c436451f4f51d1284491bb4356dd989", - "reference": "9df847980c436451f4f51d1284491bb4356dd989", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/e99af79b1e776646eda0e1c23b7b45c184ff99be", + "reference": "e99af79b1e776646eda0e1c23b7b45c184ff99be", "shasum": "" }, "require": { @@ -6708,7 +6707,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.4.13" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.14" }, "funding": [ { @@ -6728,20 +6727,20 @@ "type": "tidelift" } ], - "time": "2026-05-27T08:31:43+00:00" + "time": "2026-06-27T09:14:35+00:00" }, { "name": "symfony/mailer", - "version": "v7.4.12", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "5cefb712a25f320579615ba9e1942abaeade7dff" + "reference": "f88ce03ae73e3edb5c176ce1f337709996e88495" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/5cefb712a25f320579615ba9e1942abaeade7dff", - "reference": "5cefb712a25f320579615ba9e1942abaeade7dff", + "url": "https://api.github.com/repos/symfony/mailer/zipball/f88ce03ae73e3edb5c176ce1f337709996e88495", + "reference": "f88ce03ae73e3edb5c176ce1f337709996e88495", "shasum": "" }, "require": { @@ -6792,7 +6791,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.4.12" + "source": "https://github.com/symfony/mailer/tree/v7.4.14" }, "funding": [ { @@ -6812,7 +6811,7 @@ "type": "tidelift" } ], - "time": "2026-05-20T07:20:23+00:00" + "time": "2026-06-13T08:51:35+00:00" }, { "name": "symfony/mime", @@ -7952,16 +7951,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", "shasum": "" }, "require": { @@ -8015,7 +8014,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" }, "funding": [ { @@ -8035,7 +8034,7 @@ "type": "tidelift" } ], - "time": "2026-03-28T09:44:51+00:00" + "time": "2026-06-16T09:55:08+00:00" }, { "name": "symfony/string", @@ -8129,16 +8128,16 @@ }, { "name": "symfony/translation", - "version": "v8.1.0", + "version": "v8.1.1", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "b2bd012ca28c4acae830ee1206a5b6e35dd99693" + "reference": "342b4218630dc2cf284cedcb2080c80b13404014" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/b2bd012ca28c4acae830ee1206a5b6e35dd99693", - "reference": "b2bd012ca28c4acae830ee1206a5b6e35dd99693", + "url": "https://api.github.com/repos/symfony/translation/zipball/342b4218630dc2cf284cedcb2080c80b13404014", + "reference": "342b4218630dc2cf284cedcb2080c80b13404014", "shasum": "" }, "require": { @@ -8198,7 +8197,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v8.1.0" + "source": "https://github.com/symfony/translation/tree/v8.1.1" }, "funding": [ { @@ -8218,20 +8217,20 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-06-06T11:11:44+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", - "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621", "shasum": "" }, "require": { @@ -8280,7 +8279,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.1" }, "funding": [ { @@ -8300,7 +8299,7 @@ "type": "tidelift" } ], - "time": "2026-01-05T13:30:16+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/uid", @@ -8382,16 +8381,16 @@ }, { "name": "symfony/var-dumper", - "version": "v7.4.8", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd" + "reference": "9a3a56a4a1e65a5cb4f8d13801fe8ab0a170e358" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9510c3966f749a1d1ff0059e1eabef6cc621e7fd", - "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9a3a56a4a1e65a5cb4f8d13801fe8ab0a170e358", + "reference": "9a3a56a4a1e65a5cb4f8d13801fe8ab0a170e358", "shasum": "" }, "require": { @@ -8445,7 +8444,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.8" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.14" }, "funding": [ { @@ -8465,7 +8464,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T13:44:50+00:00" + "time": "2026-06-08T20:24:16+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -8524,16 +8523,16 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.6.3", + "version": "v5.6.4", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "955e7815d677a3eaa7075231212f2110983adecc" + "reference": "416df702837983f8d5ff48c9c3fee4f5f57b980b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", - "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/416df702837983f8d5ff48c9c3fee4f5f57b980b", + "reference": "416df702837983f8d5ff48c9c3fee4f5f57b980b", "shasum": "" }, "require": { @@ -8592,7 +8591,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.4" }, "funding": [ { @@ -8604,7 +8603,7 @@ "type": "tidelift" } ], - "time": "2025-12-27T19:49:13+00:00" + "time": "2026-07-06T19:11:50+00:00" }, { "name": "voku/portable-ascii", @@ -9704,24 +9703,24 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.55", + "version": "11.5.56", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + "reference": "5f83edffa6967c3db468d48a695ec7bcb02e9256" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", - "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5f83edffa6967c3db468d48a695ec7bcb02e9256", + "reference": "5f83edffa6967c3db468d48a695ec7bcb02e9256", "shasum": "" }, "require": { "ext-dom": "*", + "ext-filter": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", - "ext-xml": "*", "ext-xmlwriter": "*", "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", @@ -9786,31 +9785,15 @@ "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.55" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.56" }, "funding": [ { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", - "type": "tidelift" + "url": "https://phpunit.de/sponsoring.html", + "type": "other" } ], - "time": "2026-02-18T12:37:06+00:00" + "time": "2026-07-06T14:52:39+00:00" }, { "name": "sebastian/cli-parser", @@ -11310,16 +11293,16 @@ }, { "name": "symfony/yaml", - "version": "v8.1.0", + "version": "v8.1.1", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "efb42bd2c6f4f3ccfd4683583449938b5fc146b0" + "reference": "8e4cdd4311683516be06944f4b85244063cdb886" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/efb42bd2c6f4f3ccfd4683583449938b5fc146b0", - "reference": "efb42bd2c6f4f3ccfd4683583449938b5fc146b0", + "url": "https://api.github.com/repos/symfony/yaml/zipball/8e4cdd4311683516be06944f4b85244063cdb886", + "reference": "8e4cdd4311683516be06944f4b85244063cdb886", "shasum": "" }, "require": { @@ -11362,7 +11345,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v8.1.0" + "source": "https://github.com/symfony/yaml/tree/v8.1.1" }, "funding": [ { @@ -11382,7 +11365,7 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-06-09T11:06:24+00:00" }, { "name": "theseer/tokenizer", From 905f0257bf83d30baa9b69ffbf4e33d31caf9965 Mon Sep 17 00:00:00 2001 From: dogukanoksuz Date: Fri, 10 Jul 2026 07:20:28 +0000 Subject: [PATCH 11/11] chore: update version --- storage/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage/VERSION b/storage/VERSION index 69c49c72..bb576dbd 100644 --- a/storage/VERSION +++ b/storage/VERSION @@ -1 +1 @@ -2.3-dev +2.3