فهرست منبع

feat: added OIDC support - alpha testing

causefx@me.com 2 ماه پیش
والد
کامیت
66a563d3eb

+ 94 - 0
api/classes/organizr.class.php

@@ -17,6 +17,7 @@ class Organizr
 	use NetDataFunctions;
 	use NormalFunctions;
 	use OAuthFunctions;
+	use OIDCFunctions;
 	use OptionsFunction;
 	use OrganizrFunctions;
 	use PluginFunctions;
@@ -1670,6 +1671,34 @@ class Organizr
 		return $this->processQueries($response);
 	}
 
+	public function getUserByUsername($username)
+	{
+		$response = [
+			array(
+				'function' => 'fetch',
+				'query' => array(
+					'SELECT * FROM users WHERE username = ? COLLATE NOCASE',
+					$username
+				)
+			)
+		];
+		return $this->processQueries($response);
+	}
+
+	public function updateUserById($id, $updates)
+	{
+		if (empty($updates) || !$id) {
+			return false;
+		}
+		try {
+			$this->db->query('UPDATE [users] SET', $updates, 'WHERE id = ?', $id);
+			return true;
+		} catch (Exception $e) {
+			$this->setLoggerChannel('User')->error('Failed to update user: ' . $e->getMessage());
+			return false;
+		}
+	}
+
 	protected function invalidToken($token)
 	{
 		if (isset($_COOKIE[$this->cookieName])) {
@@ -2624,6 +2653,71 @@ class Organizr
 				$this->settingsOption('password', 'komgaFallbackPassword', ['label' => 'Komga Fallback Password']),
 				$this->settingsOption('password', 'komgaSSOMasterPassword', ['label' => 'Komga Master Password', 'help' => 'Sets master password if using oAuth backend - This will set the password on the login form for logins using oAuth where no password is supplied.']),
 			],
+			'OIDC Global' => [
+				$this->settingsOption('enable', 'oidcEnabled', ['label' => 'Enable OIDC Authentication']),
+				$this->settingsOption('switch', 'oidcAutoRedirect', ['label' => 'Auto-Redirect to OIDC', 'help' => 'Automatically redirect to OIDC provider on login page (add #noredirect to URL to bypass)']),
+				$this->settingsOption('select', 'oidcAutoRedirectProvider', ['label' => 'Auto-Redirect Provider', 'options' => [
+					['name' => 'None', 'value' => ''],
+					['name' => 'Authentik', 'value' => 'authentik'],
+					['name' => 'Keycloak', 'value' => 'keycloak'],
+					['name' => 'PocketID', 'value' => 'pocketid'],
+					['name' => 'Zitadel', 'value' => 'zitadel'],
+				]]),
+				$this->settingsOption('url', 'oidcAutoRedirectLogoutUrl', ['label' => 'Custom Logout URL', 'help' => 'Redirect to this URL after logout (e.g., OIDC provider logout endpoint)']),
+				$this->settingsOption('blank'),
+				$this->settingsOption('switch', 'oidcAutoCreateUsers', ['label' => 'Auto-Create Users', 'help' => 'Automatically create users on first OIDC login']),
+				$this->settingsOption('switch', 'oidcLinkExistingUsers', ['label' => 'Link Existing Users', 'help' => 'Link OIDC accounts to existing Organizr users by email']),
+				$this->settingsOption('switch', 'oidcUpdateGroupsOnLogin', ['label' => 'Update Groups on Login', 'help' => 'Re-sync group membership from OIDC on each login']),
+				$this->settingsOption('select', 'oidcDefaultGroupId', ['label' => 'Default Group', 'help' => 'Group assigned to new OIDC users if no mapping matches', 'options' => $this->groupSelect()]),
+				$this->settingsOption('blank'),
+				$this->settingsOption('input', 'oidcGroupClaimName', ['label' => 'Group Claim Name', 'placeholder' => 'groups', 'help' => 'JWT claim containing user groups']),
+				$this->settingsOption('select', 'oidcGroupMappingMode', ['label' => 'Group Mapping Mode', 'help' => 'How to handle multiple matching groups', 'options' => [
+					['name' => 'First Match', 'value' => 'first'],
+					['name' => 'Highest Privilege (lowest ID)', 'value' => 'highest_privilege'],
+					['name' => 'Lowest Privilege (highest ID)', 'value' => 'lowest_privilege'],
+				]]),
+				$this->settingsOption('textbox', 'oidcGroupMappings', ['label' => 'Group Mappings (JSON)', 'help' => 'Map OIDC groups to Organizr group IDs. Example: {"oidc-admins": 0, "oidc-users": 3}']),
+			],
+			'OIDC: Authentik' => [
+				$this->settingsOption('enable', 'oidcAuthentikEnabled', ['label' => 'Enable Authentik']),
+				$this->settingsOption('input', 'oidcAuthentikName', ['label' => 'Display Name', 'placeholder' => 'Authentik']),
+				$this->settingsOption('url', 'oidcAuthentikDiscoveryUrl', ['label' => 'Discovery URL', 'help' => 'e.g., https://authentik.example.com/application/o/organizr/.well-known/openid-configuration']),
+				$this->settingsOption('input', 'oidcAuthentikClientId', ['label' => 'Client ID']),
+				$this->settingsOption('password', 'oidcAuthentikClientSecret', ['label' => 'Client Secret']),
+				$this->settingsOption('input', 'oidcAuthentikScopes', ['label' => 'Scopes', 'placeholder' => 'openid,profile,email,groups']),
+				$this->settingsOption('input', 'oidcAuthentikGroupClaim', ['label' => 'Group Claim Override', 'placeholder' => 'groups', 'help' => 'Leave empty to use global setting']),
+				$this->settingsOption('button', 'testOIDCAuthentik', ['label' => 'Test Connection', 'icon' => 'fa fa-plug', 'text' => 'Test', 'attr' => 'onclick="testOIDCConnection(\'authentik\')"']),
+			],
+			'OIDC: Keycloak' => [
+				$this->settingsOption('enable', 'oidcKeycloakEnabled', ['label' => 'Enable Keycloak']),
+				$this->settingsOption('input', 'oidcKeycloakName', ['label' => 'Display Name', 'placeholder' => 'Keycloak']),
+				$this->settingsOption('url', 'oidcKeycloakDiscoveryUrl', ['label' => 'Discovery URL', 'help' => 'e.g., https://keycloak.example.com/realms/master/.well-known/openid-configuration']),
+				$this->settingsOption('input', 'oidcKeycloakClientId', ['label' => 'Client ID']),
+				$this->settingsOption('password', 'oidcKeycloakClientSecret', ['label' => 'Client Secret']),
+				$this->settingsOption('input', 'oidcKeycloakScopes', ['label' => 'Scopes', 'placeholder' => 'openid,profile,email']),
+				$this->settingsOption('input', 'oidcKeycloakGroupClaim', ['label' => 'Group Claim Override', 'placeholder' => 'groups', 'help' => 'Leave empty to use global setting']),
+				$this->settingsOption('button', 'testOIDCKeycloak', ['label' => 'Test Connection', 'icon' => 'fa fa-plug', 'text' => 'Test', 'attr' => 'onclick="testOIDCConnection(\'keycloak\')"']),
+			],
+			'OIDC: PocketID' => [
+				$this->settingsOption('enable', 'oidcPocketidEnabled', ['label' => 'Enable PocketID']),
+				$this->settingsOption('input', 'oidcPocketidName', ['label' => 'Display Name', 'placeholder' => 'PocketID']),
+				$this->settingsOption('url', 'oidcPocketidDiscoveryUrl', ['label' => 'Discovery URL', 'help' => 'e.g., https://pocketid.example.com/.well-known/openid-configuration']),
+				$this->settingsOption('input', 'oidcPocketidClientId', ['label' => 'Client ID']),
+				$this->settingsOption('password', 'oidcPocketidClientSecret', ['label' => 'Client Secret']),
+				$this->settingsOption('input', 'oidcPocketidScopes', ['label' => 'Scopes', 'placeholder' => 'openid,profile,email']),
+				$this->settingsOption('input', 'oidcPocketidGroupClaim', ['label' => 'Group Claim Override', 'placeholder' => 'groups', 'help' => 'Leave empty to use global setting']),
+				$this->settingsOption('button', 'testOIDCPocketid', ['label' => 'Test Connection', 'icon' => 'fa fa-plug', 'text' => 'Test', 'attr' => 'onclick="testOIDCConnection(\'pocketid\')"']),
+			],
+			'OIDC: Zitadel' => [
+				$this->settingsOption('enable', 'oidcZitadelEnabled', ['label' => 'Enable Zitadel']),
+				$this->settingsOption('input', 'oidcZitadelName', ['label' => 'Display Name', 'placeholder' => 'Zitadel']),
+				$this->settingsOption('url', 'oidcZitadelDiscoveryUrl', ['label' => 'Discovery URL', 'help' => 'e.g., https://zitadel.example.com/.well-known/openid-configuration']),
+				$this->settingsOption('input', 'oidcZitadelClientId', ['label' => 'Client ID']),
+				$this->settingsOption('password', 'oidcZitadelClientSecret', ['label' => 'Client Secret']),
+				$this->settingsOption('input', 'oidcZitadelScopes', ['label' => 'Scopes', 'placeholder' => 'openid,profile,email,urn:zitadel:iam:org:project:roles']),
+				$this->settingsOption('input', 'oidcZitadelGroupClaim', ['label' => 'Group Claim Override', 'placeholder' => 'urn:zitadel:iam:org:project:roles', 'help' => 'Zitadel uses a special claim format for roles']),
+				$this->settingsOption('button', 'testOIDCZitadel', ['label' => 'Test Connection', 'icon' => 'fa fa-plug', 'text' => 'Test', 'attr' => 'onclick="testOIDCConnection(\'zitadel\')"']),
+			],
 		];
 	}
 

+ 7 - 1
api/composer.json

@@ -22,6 +22,12 @@
     "peppeocchi/php-cron-scheduler": "^4.0",
     "simshaun/recurr": "^5.0",
     "stripe/stripe-php": "^7.116",
-    "ramsey/uuid": "^4.2"
+    "ramsey/uuid": "^4.2",
+    "league/oauth2-client": "^2.7"
+  },
+  "config": {
+    "platform": {
+      "php": "7.4.0"
+    }
   }
 }

+ 18 - 15
api/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": "ca4f53441305ffa3e09c532006fa9c19",
+    "content-hash": "09e533e3077f054c098497069a935661",
     "packages": [
         {
             "name": "adldap2/adldap2",
@@ -1215,27 +1215,27 @@
         },
         {
             "name": "league/oauth2-client",
-            "version": "2.6.0",
+            "version": "2.7.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/oauth2-client.git",
-                "reference": "badb01e62383430706433191b82506b6df24ad98"
+                "reference": "160d6274b03562ebeb55ed18399281d8118b76c8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/badb01e62383430706433191b82506b6df24ad98",
-                "reference": "badb01e62383430706433191b82506b6df24ad98",
+                "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/160d6274b03562ebeb55ed18399281d8118b76c8",
+                "reference": "160d6274b03562ebeb55ed18399281d8118b76c8",
                 "shasum": ""
             },
             "require": {
-                "guzzlehttp/guzzle": "^7.0",
+                "guzzlehttp/guzzle": "^6.0 || ^7.0",
                 "paragonie/random_compat": "^1 || ^2 || ^9.99",
                 "php": "^5.6 || ^7.0 || ^8.0"
             },
             "require-dev": {
-                "mockery/mockery": "^1.3",
-                "php-parallel-lint/php-parallel-lint": "^1.2",
-                "phpunit/phpunit": "^5.7 || ^6.0 || ^9.3",
+                "mockery/mockery": "^1.3.5",
+                "php-parallel-lint/php-parallel-lint": "^1.3.1",
+                "phpunit/phpunit": "^5.7 || ^6.0 || ^9.5",
                 "squizlabs/php_codesniffer": "^2.3 || ^3.0"
             },
             "type": "library",
@@ -1279,9 +1279,9 @@
             ],
             "support": {
                 "issues": "https://github.com/thephpleague/oauth2-client/issues",
-                "source": "https://github.com/thephpleague/oauth2-client/tree/2.6.0"
+                "source": "https://github.com/thephpleague/oauth2-client/tree/2.7.0"
             },
-            "time": "2020-10-28T02:03:40+00:00"
+            "time": "2023-04-16T18:19:15+00:00"
         },
         {
             "name": "monolog/monolog",
@@ -4146,10 +4146,13 @@
     "packages-dev": [],
     "aliases": [],
     "minimum-stability": "stable",
-    "stability-flags": [],
+    "stability-flags": {},
     "prefer-stable": false,
     "prefer-lowest": false,
-    "platform": [],
-    "platform-dev": [],
-    "plugin-api-version": "2.3.0"
+    "platform": {},
+    "platform-dev": {},
+    "platform-overrides": {
+        "php": "7.4.0"
+    },
+    "plugin-api-version": "2.9.0"
 }

+ 45 - 1
api/config/default.php

@@ -770,5 +770,49 @@ return [
 	'socksDebug' => false,
 	'maxSocksDebugSize' => 100,
 	'bypassLoginForLocal' => false,
-	'localLoginUserId' => "1"
+	'localLoginUserId' => "1",
+	// OIDC Global Settings
+	'oidcEnabled' => false,
+	'oidcAutoRedirect' => false,
+	'oidcAutoRedirectProvider' => '',
+	'oidcAutoRedirectLogoutUrl' => '',
+	'oidcAutoCreateUsers' => true,
+	'oidcLinkExistingUsers' => true,
+	'oidcDefaultGroupId' => null,
+	'oidcGroupClaimName' => 'groups',
+	'oidcGroupMappings' => '{}',
+	'oidcGroupMappingMode' => 'first',
+	'oidcUpdateGroupsOnLogin' => false,
+	// OIDC Authentik Provider
+	'oidcAuthentikEnabled' => false,
+	'oidcAuthentikName' => 'Authentik',
+	'oidcAuthentikClientId' => '',
+	'oidcAuthentikClientSecret' => '',
+	'oidcAuthentikDiscoveryUrl' => '',
+	'oidcAuthentikScopes' => 'openid,profile,email,groups',
+	'oidcAuthentikGroupClaim' => '',
+	// OIDC Keycloak Provider
+	'oidcKeycloakEnabled' => false,
+	'oidcKeycloakName' => 'Keycloak',
+	'oidcKeycloakClientId' => '',
+	'oidcKeycloakClientSecret' => '',
+	'oidcKeycloakDiscoveryUrl' => '',
+	'oidcKeycloakScopes' => 'openid,profile,email',
+	'oidcKeycloakGroupClaim' => '',
+	// OIDC PocketID Provider
+	'oidcPocketidEnabled' => false,
+	'oidcPocketidName' => 'PocketID',
+	'oidcPocketidClientId' => '',
+	'oidcPocketidClientSecret' => '',
+	'oidcPocketidDiscoveryUrl' => '',
+	'oidcPocketidScopes' => 'openid,profile,email',
+	'oidcPocketidGroupClaim' => '',
+	// OIDC Zitadel Provider
+	'oidcZitadelEnabled' => false,
+	'oidcZitadelName' => 'Zitadel',
+	'oidcZitadelClientId' => '',
+	'oidcZitadelClientSecret' => '',
+	'oidcZitadelDiscoveryUrl' => '',
+	'oidcZitadelScopes' => 'openid,profile,email,urn:zitadel:iam:org:project:roles',
+	'oidcZitadelGroupClaim' => 'urn:zitadel:iam:org:project:roles',
 ];

+ 608 - 0
api/functions/oidc-functions.php

@@ -0,0 +1,608 @@
+<?php
+
+trait OIDCFunctions
+{
+	/**
+	 * Provider registry with provider-specific configurations
+	 */
+	private $oidcProviders = [
+		'authentik' => [
+			'configPrefix' => 'oidcAuthentik',
+			'defaultGroupClaim' => 'groups',
+			'supportsEndSession' => true,
+		],
+		'keycloak' => [
+			'configPrefix' => 'oidcKeycloak',
+			'defaultGroupClaim' => 'groups',
+			'supportsEndSession' => true,
+		],
+		'pocketid' => [
+			'configPrefix' => 'oidcPocketid',
+			'defaultGroupClaim' => 'groups',
+			'supportsEndSession' => false,
+		],
+		'zitadel' => [
+			'configPrefix' => 'oidcZitadel',
+			'defaultGroupClaim' => 'urn:zitadel:iam:org:project:roles',
+			'supportsEndSession' => true,
+			'rolesAsObject' => true,
+		],
+	];
+
+	/**
+	 * Get list of enabled OIDC providers
+	 */
+	public function getEnabledOIDCProviders()
+	{
+		$enabled = [];
+		if (!$this->config['oidcEnabled']) {
+			return $enabled;
+		}
+		foreach ($this->oidcProviders as $provider => $config) {
+			$enabledKey = $config['configPrefix'] . 'Enabled';
+			if ($this->config[$enabledKey] ?? false) {
+				$enabled[$provider] = $config;
+			}
+		}
+		return $enabled;
+	}
+
+	/**
+	 * Get provider configuration
+	 */
+	public function getOIDCProviderConfig($provider)
+	{
+		if (!isset($this->oidcProviders[$provider])) {
+			return null;
+		}
+		$config = $this->oidcProviders[$provider];
+		$prefix = $config['configPrefix'];
+		return [
+			'provider' => $provider,
+			'name' => $this->config[$prefix . 'Name'] ?? ucfirst($provider),
+			'clientId' => $this->config[$prefix . 'ClientId'] ?? '',
+			'clientSecret' => $this->decrypt($this->config[$prefix . 'ClientSecret'] ?? ''),
+			'discoveryUrl' => $this->config[$prefix . 'DiscoveryUrl'] ?? '',
+			'scopes' => $this->config[$prefix . 'Scopes'] ?? 'openid,profile,email',
+			'groupClaim' => $this->config[$prefix . 'GroupClaim'] ?: ($config['defaultGroupClaim'] ?? 'groups'),
+			'supportsEndSession' => $config['supportsEndSession'] ?? false,
+			'rolesAsObject' => $config['rolesAsObject'] ?? false,
+		];
+	}
+
+	/**
+	 * Fetch and cache OIDC discovery document
+	 */
+	public function getOIDCDiscovery($provider)
+	{
+		$config = $this->getOIDCProviderConfig($provider);
+		if (!$config || empty($config['discoveryUrl'])) {
+			$this->setLoggerChannel('OIDC')->error('Discovery URL not configured for provider: ' . $provider);
+			return null;
+		}
+		$cacheKey = 'oidc_discovery_' . $provider;
+		$cached = $_SESSION[$cacheKey] ?? null;
+		if ($cached && (time() - ($cached['fetched_at'] ?? 0)) < 3600) {
+			return $cached['data'];
+		}
+		try {
+			$response = Requests::get($config['discoveryUrl'], [], [
+				'verify' => $this->getCert(),
+				'timeout' => 10,
+			]);
+			if ($response->success) {
+				$discovery = json_decode($response->body, true);
+				$_SESSION[$cacheKey] = [
+					'data' => $discovery,
+					'fetched_at' => time(),
+				];
+				return $discovery;
+			}
+			$this->setLoggerChannel('OIDC')->error('Failed to fetch discovery document: ' . $response->status_code);
+			return null;
+		} catch (Requests_Exception $e) {
+			$this->setLoggerChannel('OIDC')->error($e);
+			return null;
+		}
+	}
+
+	/**
+	 * Generate PKCE code verifier and challenge
+	 */
+	public function generatePKCE()
+	{
+		$codeVerifier = bin2hex(random_bytes(32));
+		$codeChallenge = rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '=');
+		return [
+			'code_verifier' => $codeVerifier,
+			'code_challenge' => $codeChallenge,
+			'code_challenge_method' => 'S256',
+		];
+	}
+
+	/**
+	 * Generate state parameter for CSRF protection
+	 */
+	public function generateOIDCState($provider)
+	{
+		$state = bin2hex(random_bytes(16));
+		$_SESSION['oidc_state'] = $state;
+		$_SESSION['oidc_state_provider'] = $provider;
+		$_SESSION['oidc_state_time'] = time();
+		return $state;
+	}
+
+	/**
+	 * Validate state parameter
+	 */
+	public function validateOIDCState($state, $provider)
+	{
+		$storedState = $_SESSION['oidc_state'] ?? null;
+		$storedProvider = $_SESSION['oidc_state_provider'] ?? null;
+		$stateTime = $_SESSION['oidc_state_time'] ?? 0;
+		// State expires after 10 minutes
+		if (time() - $stateTime > 600) {
+			$this->setLoggerChannel('OIDC')->warning('OIDC state expired');
+			return false;
+		}
+		if (!hash_equals($storedState ?? '', $state) || $storedProvider !== $provider) {
+			$this->setLoggerChannel('OIDC')->warning('OIDC state mismatch');
+			return false;
+		}
+		// Clear used state
+		unset($_SESSION['oidc_state'], $_SESSION['oidc_state_provider'], $_SESSION['oidc_state_time']);
+		return true;
+	}
+
+	/**
+	 * Generate authorization URL for OIDC provider
+	 */
+	public function getOIDCAuthorizationUrl($provider)
+	{
+		$config = $this->getOIDCProviderConfig($provider);
+		if (!$config) {
+			return null;
+		}
+		$discovery = $this->getOIDCDiscovery($provider);
+		if (!$discovery || empty($discovery['authorization_endpoint'])) {
+			$this->setLoggerChannel('OIDC')->error('Authorization endpoint not found in discovery');
+			return null;
+		}
+		$pkce = $this->generatePKCE();
+		$_SESSION['oidc_code_verifier'] = $pkce['code_verifier'];
+		$state = $this->generateOIDCState($provider);
+		$nonce = bin2hex(random_bytes(16));
+		$_SESSION['oidc_nonce'] = $nonce;
+		$scopes = str_replace(',', ' ', $config['scopes']);
+		$params = [
+			'response_type' => 'code',
+			'client_id' => $config['clientId'],
+			'redirect_uri' => $this->getServerPath() . 'api/v2/oidc/' . $provider . '/callback',
+			'scope' => $scopes,
+			'state' => $state,
+			'nonce' => $nonce,
+			'code_challenge' => $pkce['code_challenge'],
+			'code_challenge_method' => $pkce['code_challenge_method'],
+		];
+		return $discovery['authorization_endpoint'] . '?' . http_build_query($params);
+	}
+
+	/**
+	 * Exchange authorization code for tokens
+	 */
+	public function exchangeOIDCCode($provider, $code)
+	{
+		$config = $this->getOIDCProviderConfig($provider);
+		if (!$config) {
+			return null;
+		}
+		$discovery = $this->getOIDCDiscovery($provider);
+		if (!$discovery || empty($discovery['token_endpoint'])) {
+			$this->setLoggerChannel('OIDC')->error('Token endpoint not found in discovery');
+			return null;
+		}
+		$codeVerifier = $_SESSION['oidc_code_verifier'] ?? '';
+		unset($_SESSION['oidc_code_verifier']);
+		$data = [
+			'grant_type' => 'authorization_code',
+			'code' => $code,
+			'redirect_uri' => $this->getServerPath() . 'api/v2/oidc/' . $provider . '/callback',
+			'client_id' => $config['clientId'],
+			'client_secret' => $config['clientSecret'],
+			'code_verifier' => $codeVerifier,
+		];
+		try {
+			$response = Requests::post($discovery['token_endpoint'], [
+				'Content-Type' => 'application/x-www-form-urlencoded',
+			], http_build_query($data), [
+				'verify' => $this->getCert(),
+				'timeout' => 10,
+			]);
+			if ($response->success) {
+				$tokens = json_decode($response->body, true);
+				$this->setLoggerChannel('OIDC')->debug('Token exchange successful for provider: ' . $provider);
+				return $tokens;
+			}
+			$this->setLoggerChannel('OIDC')->error('Token exchange failed: ' . $response->body);
+			return null;
+		} catch (Requests_Exception $e) {
+			$this->setLoggerChannel('OIDC')->error($e);
+			return null;
+		}
+	}
+
+	/**
+	 * Get user info from OIDC provider
+	 */
+	public function getOIDCUserInfo($provider, $accessToken)
+	{
+		$discovery = $this->getOIDCDiscovery($provider);
+		if (!$discovery || empty($discovery['userinfo_endpoint'])) {
+			$this->setLoggerChannel('OIDC')->warning('Userinfo endpoint not found, using ID token claims');
+			return null;
+		}
+		try {
+			$response = Requests::get($discovery['userinfo_endpoint'], [
+				'Authorization' => 'Bearer ' . $accessToken,
+			], [
+				'verify' => $this->getCert(),
+				'timeout' => 10,
+			]);
+			if ($response->success) {
+				return json_decode($response->body, true);
+			}
+			$this->setLoggerChannel('OIDC')->error('Userinfo request failed: ' . $response->status_code);
+			return null;
+		} catch (Requests_Exception $e) {
+			$this->setLoggerChannel('OIDC')->error($e);
+			return null;
+		}
+	}
+
+	/**
+	 * Decode JWT ID token (without verification - tokens already verified by provider)
+	 */
+	public function decodeOIDCIdToken($idToken)
+	{
+		$parts = explode('.', $idToken);
+		if (count($parts) !== 3) {
+			return null;
+		}
+		$payload = json_decode(base64_decode(strtr($parts[1], '-_', '+/')), true);
+		return $payload;
+	}
+
+	/**
+	 * Extract groups from OIDC claims
+	 */
+	public function extractOIDCGroups($provider, $claims)
+	{
+		$config = $this->getOIDCProviderConfig($provider);
+		$groupClaim = $config['groupClaim'] ?? $this->config['oidcGroupClaimName'];
+		$groups = $claims[$groupClaim] ?? [];
+		// Handle Zitadel's role format (object keys)
+		if ($provider === 'zitadel' && ($config['rolesAsObject'] ?? false)) {
+			if (is_array($groups) && !empty($groups) && !isset($groups[0])) {
+				$groups = array_keys($groups);
+			}
+		}
+		// Ensure array format
+		if (is_string($groups)) {
+			$groups = [$groups];
+		}
+		return $groups;
+	}
+
+	/**
+	 * Map OIDC groups to Organizr group ID
+	 */
+	public function mapOIDCGroupToOrganizr($oidcGroups)
+	{
+		$mappings = json_decode($this->config['oidcGroupMappings'] ?? '{}', true) ?: [];
+		$mode = $this->config['oidcGroupMappingMode'] ?? 'first';
+		$matchedGroups = [];
+		foreach ($oidcGroups as $oidcGroup) {
+			// Direct match
+			if (isset($mappings[$oidcGroup])) {
+				$matchedGroups[] = (int)$mappings[$oidcGroup];
+				continue;
+			}
+			// Try matching without leading slash (Keycloak nested groups)
+			$stripped = ltrim($oidcGroup, '/');
+			if (isset($mappings[$stripped])) {
+				$matchedGroups[] = (int)$mappings[$stripped];
+				continue;
+			}
+			// Try matching last segment only
+			$segments = explode('/', $stripped);
+			$lastSegment = end($segments);
+			if (isset($mappings[$lastSegment])) {
+				$matchedGroups[] = (int)$mappings[$lastSegment];
+			}
+		}
+		if (empty($matchedGroups)) {
+			// Fallback to default group
+			$defaultGroupId = $this->config['oidcDefaultGroupId'];
+			if ($defaultGroupId !== null && $defaultGroupId !== '') {
+				return (int)$defaultGroupId;
+			}
+			// Use system default
+			return $this->getDefaultGroupId();
+		}
+		switch ($mode) {
+			case 'highest_privilege':
+				// Lower group_id = higher privilege in Organizr (0 = admin)
+				return min($matchedGroups);
+			case 'lowest_privilege':
+				return max($matchedGroups);
+			case 'first':
+			default:
+				return $matchedGroups[0];
+		}
+	}
+
+	/**
+	 * Get default group ID from system settings
+	 */
+	private function getDefaultGroupId()
+	{
+		// Default to group ID 3 (User) if not configured
+		return 3;
+	}
+
+	/**
+	 * Create or link user from OIDC claims
+	 */
+	public function createOrLinkOIDCUser($provider, $userInfo, $oidcGroups)
+	{
+		$email = $userInfo['email'] ?? '';
+		$username = $userInfo['preferred_username'] ?? $userInfo['name'] ?? $userInfo['sub'] ?? '';
+		$image = $userInfo['picture'] ?? '';
+		if (empty($username)) {
+			$this->setLoggerChannel('OIDC')->error('No username available from OIDC claims');
+			return null;
+		}
+		$groupId = $this->mapOIDCGroupToOrganizr($oidcGroups);
+		$group = $this->getGroupById($groupId);
+		$groupName = $group['group'] ?? 'User';
+		// Check if user exists by username
+		$existingUser = $this->getUserByUsername($username);
+		if ($existingUser) {
+			// Update auth_service and optionally group
+			$updates = ['auth_service' => 'oidc::' . $provider];
+			if ($this->config['oidcUpdateGroupsOnLogin']) {
+				$updates['group_id'] = $groupId;
+				$updates['group'] = $groupName;
+			}
+			$this->updateUserById($existingUser['id'], $updates);
+			$this->setLoggerChannel('OIDC')->info('Linked existing user: ' . $username);
+			return $existingUser;
+		}
+		// Check if user exists by email and linking is enabled
+		if (!empty($email) && $this->config['oidcLinkExistingUsers']) {
+			$existingUser = $this->getUserByEmail($email);
+			if ($existingUser) {
+				$updates = ['auth_service' => 'oidc::' . $provider];
+				if ($this->config['oidcUpdateGroupsOnLogin']) {
+					$updates['group_id'] = $groupId;
+					$updates['group'] = $groupName;
+				}
+				$this->updateUserById($existingUser['id'], $updates);
+				$this->setLoggerChannel('OIDC')->info('Linked user by email: ' . $email);
+				return $existingUser;
+			}
+		}
+		// Create new user if auto-create is enabled
+		if (!$this->config['oidcAutoCreateUsers']) {
+			$this->setLoggerChannel('OIDC')->warning('User not found and auto-create disabled: ' . $username);
+			return null;
+		}
+		// Generate random password (user won't use it)
+		$password = bin2hex(random_bytes(16));
+		$userInfo = [
+			'username' => $username,
+			'password' => password_hash($password, PASSWORD_BCRYPT),
+			'email' => $email,
+			'group' => $groupName,
+			'group_id' => $groupId,
+			'image' => $image ?: $this->gravatar($email),
+			'register_date' => $this->currentTime,
+			'auth_service' => 'oidc::' . $provider,
+		];
+		try {
+			$this->db->query('INSERT INTO [users]', $userInfo);
+			$this->setLoggerChannel('OIDC')->info('Created new OIDC user: ' . $username);
+			return $this->getUserByUsername($username);
+		} catch (Exception $e) {
+			$this->setLoggerChannel('OIDC')->error('Failed to create user: ' . $e->getMessage());
+			return null;
+		}
+	}
+
+	/**
+	 * Process OIDC callback
+	 */
+	public function processOIDCCallback($provider, $code, $state)
+	{
+		// Validate state
+		if (!$this->validateOIDCState($state, $provider)) {
+			$this->setAPIResponse('error', 'Invalid or expired state parameter', 400);
+			return false;
+		}
+		// Exchange code for tokens
+		$tokens = $this->exchangeOIDCCode($provider, $code);
+		if (!$tokens || empty($tokens['access_token'])) {
+			$this->setAPIResponse('error', 'Failed to exchange authorization code', 500);
+			return false;
+		}
+		// Get user info from userinfo endpoint or ID token
+		$userInfo = $this->getOIDCUserInfo($provider, $tokens['access_token']);
+		if (!$userInfo && !empty($tokens['id_token'])) {
+			$userInfo = $this->decodeOIDCIdToken($tokens['id_token']);
+		}
+		if (!$userInfo) {
+			$this->setAPIResponse('error', 'Failed to get user information', 500);
+			return false;
+		}
+		// Validate nonce if present
+		$storedNonce = $_SESSION['oidc_nonce'] ?? null;
+		unset($_SESSION['oidc_nonce']);
+		if ($storedNonce && isset($userInfo['nonce']) && !hash_equals($storedNonce, $userInfo['nonce'])) {
+			$this->setLoggerChannel('OIDC')->warning('Nonce mismatch');
+			$this->setAPIResponse('error', 'Invalid nonce', 400);
+			return false;
+		}
+		// Extract groups
+		$groups = $this->extractOIDCGroups($provider, $userInfo);
+		// Create or link user
+		$user = $this->createOrLinkOIDCUser($provider, $userInfo, $groups);
+		if (!$user) {
+			$this->setAPIResponse('error', 'Failed to create or link user', 500);
+			return false;
+		}
+		// Create Organizr token
+		$this->createToken($user['username'], $user['email'], $this->config['rememberMeDays']);
+		$this->setLoggerChannel('OIDC')->info('OIDC login successful: ' . $user['username']);
+		return $user;
+	}
+
+	/**
+	 * Initiate OIDC authorization flow
+	 */
+	public function initiateOIDCFlow($provider)
+	{
+		if (!$this->config['oidcEnabled']) {
+			$this->setAPIResponse('error', 'OIDC is not enabled', 403);
+			return false;
+		}
+		$enabledProviders = $this->getEnabledOIDCProviders();
+		if (!isset($enabledProviders[$provider])) {
+			$this->setAPIResponse('error', 'Provider not enabled: ' . $provider, 404);
+			return false;
+		}
+		$authUrl = $this->getOIDCAuthorizationUrl($provider);
+		if (!$authUrl) {
+			$this->setAPIResponse('error', 'Failed to generate authorization URL', 500);
+			return false;
+		}
+		header('Location: ' . $authUrl);
+		exit;
+	}
+
+	/**
+	 * Test OIDC provider connection
+	 */
+	public function testOIDCConnection($provider)
+	{
+		$config = $this->getOIDCProviderConfig($provider);
+		if (!$config) {
+			$this->setAPIResponse('error', 'Provider not found', 404);
+			return false;
+		}
+		if (empty($config['discoveryUrl'])) {
+			$this->setAPIResponse('error', 'Discovery URL not configured', 422);
+			return false;
+		}
+		if (empty($config['clientId'])) {
+			$this->setAPIResponse('error', 'Client ID not configured', 422);
+			return false;
+		}
+		$discovery = $this->getOIDCDiscovery($provider);
+		if (!$discovery) {
+			$this->setAPIResponse('error', 'Failed to fetch discovery document', 500);
+			return false;
+		}
+		$required = ['authorization_endpoint', 'token_endpoint', 'issuer'];
+		foreach ($required as $field) {
+			if (empty($discovery[$field])) {
+				$this->setAPIResponse('error', 'Discovery document missing: ' . $field, 500);
+				return false;
+			}
+		}
+		$this->setAPIResponse('success', 'OIDC connection successful - Issuer: ' . $discovery['issuer'], 200);
+		return true;
+	}
+
+	/**
+	 * Output callback success page - redirects to Organizr root
+	 */
+	public function outputOIDCCallbackSuccess($username)
+	{
+		$this->setLoggerChannel('OIDC')->info('Redirecting user after successful login: ' . $username);
+		header('Location: ' . $this->getServerPath());
+		exit;
+	}
+
+	/**
+	 * Output callback error page
+	 */
+	public function outputOIDCCallbackError($error)
+	{
+		echo '<!DOCTYPE html>
+<html lang="en">
+<head>
+	<link rel="stylesheet" href="' . $this->getServerPath() . '/css/mvp.css">
+	<meta charset="utf-8">
+	<meta name="description" content="OIDC Login Error">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<title>OIDC Login Error</title>
+</head>
+<body>
+	<main>
+		<section>
+			<aside>
+				<h3>Login Failed</h3>
+				<p>' . htmlspecialchars($error) . '</p>
+				<p><a href="javascript:window.close()">Close this window</a></p>
+			</aside>
+		</section>
+	</main>
+</body>
+</html>';
+		exit;
+	}
+
+	/**
+	 * Get OIDC logout URL
+	 */
+	public function getOIDCLogoutUrl($provider)
+	{
+		$config = $this->getOIDCProviderConfig($provider);
+		if (!$config || !$config['supportsEndSession']) {
+			return null;
+		}
+		$discovery = $this->getOIDCDiscovery($provider);
+		if (!$discovery || empty($discovery['end_session_endpoint'])) {
+			return null;
+		}
+		$params = [
+			'client_id' => $config['clientId'],
+			'post_logout_redirect_uri' => $this->getServerPath(),
+		];
+		return $discovery['end_session_endpoint'] . '?' . http_build_query($params);
+	}
+
+	/**
+	 * Check if auto-redirect to OIDC is enabled
+	 */
+	public function shouldAutoRedirectToOIDC()
+	{
+		if (!$this->config['oidcEnabled'] || !$this->config['oidcAutoRedirect']) {
+			return false;
+		}
+		$provider = $this->config['oidcAutoRedirectProvider'] ?? '';
+		if (empty($provider)) {
+			return false;
+		}
+		$enabledProviders = $this->getEnabledOIDCProviders();
+		return isset($enabledProviders[$provider]);
+	}
+
+	/**
+	 * Get auto-redirect provider
+	 */
+	public function getAutoRedirectProvider()
+	{
+		return $this->config['oidcAutoRedirectProvider'] ?? '';
+	}
+}

+ 32 - 0
api/functions/organizr-functions.php

@@ -706,6 +706,38 @@ trait OrganizrFunctions
 		return '<a href="javascript:void(0)" class="text-center db ' . $showLogo . '" id="login-logo">' . $html . '</a>';
 	}
 
+	public function showoAuthOIDC()
+	{
+		$buttons = '';
+		$providers = $this->getEnabledOIDCProviders();
+		foreach ($providers as $provider => $config) {
+			$name = htmlspecialchars($this->config[$config['configPrefix'] . 'Name'] ?? ucfirst($provider));
+			$buttons .= '<a href="javascript:void(0)" onclick="oidcStart(\'' . htmlspecialchars($provider) . '\')" class="btn btn-lg btn-block text-uppercase waves-effect waves-light bg-oidc-' . htmlspecialchars($provider) . ' text-muted"> <span>Login with ' . $name . '</span><i aria-hidden="true" class="mdi mdi-shield-key m-l-5"></i> </a>';
+		}
+		if (!$buttons) {
+			return '';
+		}
+		return '
+		<div class="panel">
+			<div class="panel-heading bg-org" id="oidc-login-heading" role="tab">
+				<a class="panel-title" data-toggle="collapse" href="#oidc-login-collapse" data-parent="#login-panels" aria-expanded="false" aria-controls="oidc-login-collapse">
+					<i class="mdi mdi-shield-account"></i> &nbsp;
+					<span class="text-uppercase fw300" lang="en">Single Sign-On</span>
+				</a>
+			</div>
+			<div class="panel-collapse collapse in" id="oidc-login-collapse" aria-labelledby="oidc-login-heading" role="tabpanel">
+				<div class="panel-body">
+					<div class="row">
+						<div class="col-xs-12 col-sm-12 col-md-12 text-center">
+							<div class="social m-b-0">' . $buttons . '</div>
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
+		';
+	}
+
 	public function settingsDocker()
 	{
 		$type = ($this->docker) ? 'Official Docker' : 'Native';

+ 14 - 0
api/pages/login.php

@@ -14,11 +14,22 @@ function get_page_login($Organizr)
 	$hideOrganizrRecoveryPassword = ($Organizr->config['disableRecoverPass']) ? 'hidden' : '';
 	$customForgotPasswordText = (empty($Organizr->config['customForgotPassText'])) ? 'Enter your Email and instructions will be sent to you!' : $Organizr->config['customForgotPassText'];
 	$customForgotPasswordText = ($Organizr->config['disableRecoverPass']) ? 'Disabled' : $customForgotPasswordText;
+	$oidcAutoRedirectScript = '';
+	if ($Organizr->shouldAutoRedirectToOIDC()) {
+		$provider = $Organizr->getAutoRedirectProvider();
+		$oidcAutoRedirectScript = '
+// OIDC Auto-redirect
+if (!window.location.hash.includes("noredirect") && !sessionStorage.getItem("oidc_no_redirect")) {
+	window.location.href = "api/v2/oidc/' . htmlspecialchars($provider) . '/authorize";
+}
+';
+	}
 	return '
 <script>
 if(activeInfo.settings.login.rememberMe){
 	$(\'#checkbox-login\').prop(\'checked\',true);
 }
+' . $oidcAutoRedirectScript . '
 </script>
 <section id="wrapper" class="login-register">
 	<div class="login-box login-sidebar animated slideInRight">
@@ -101,6 +112,9 @@ if(activeInfo.settings.login.rememberMe){
 					<!-- PLEX OAUTH LOGIN -->
 					' . $Organizr->showoAuth() . '
 					<!-- END PLEX OAUTH LOGIN -->
+					<!-- OIDC SSO LOGIN -->
+					' . $Organizr->showoAuthOIDC() . '
+					<!-- END OIDC SSO LOGIN -->
 				</div>
 			</form>
 			<form class="form-horizontal form-material hidden" id="registerForm" onsubmit="return false;">

+ 4 - 0
api/v2/.htaccess

@@ -0,0 +1,4 @@
+RewriteEngine On
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule ^ index.php [QSA,L]

+ 2 - 1
api/v2/index.php

@@ -47,7 +47,8 @@ $GLOBALS['bypass'] = array(
 	'/api/v2/login',
 	'/api/v2/wizard/path',
 	'/api/v2/login/api',
-	'/api/v2/plex/register'
+	'/api/v2/plex/register',
+	'/api/v2/oidc/providers'
 );
 $GLOBALS['responseCode'] = 200;
 function jsonE($json)

+ 100 - 0
api/v2/routes/oidc.php

@@ -0,0 +1,100 @@
+<?php
+/**
+ * OIDC Authentication Routes
+ */
+
+// Ensure PHP sessions are started for OIDC state management
+if (session_status() === PHP_SESSION_NONE) {
+	// Configure session cookies for cross-site requests (OIDC redirect flow)
+	session_set_cookie_params([
+		'lifetime' => 600, // 10 minutes for OIDC flow
+		'path' => '/',
+		'secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
+		'httponly' => true,
+		'samesite' => 'Lax' // Lax allows the session cookie to be sent on top-level navigations
+	]);
+	session_start();
+}
+
+/**
+ * Get enabled OIDC providers (public endpoint)
+ */
+$app->get('/oidc/providers', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$providers = $Organizr->getEnabledOIDCProviders();
+	$result = [];
+	foreach ($providers as $provider => $config) {
+		$result[$provider] = [
+			'name' => $Organizr->config[$config['configPrefix'] . 'Name'] ?? ucfirst($provider),
+			'enabled' => true,
+		];
+	}
+	$GLOBALS['api']['response']['data'] = $result;
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+
+/**
+ * Initiate OIDC authorization flow
+ */
+$app->get('/oidc/{provider}/authorize', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$provider = $args['provider'] ?? '';
+	// This will redirect to the provider, exit happens in initiateOIDCFlow
+	$Organizr->initiateOIDCFlow($provider);
+	// If we get here, there was an error
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+
+/**
+ * OIDC callback handler
+ */
+$app->get('/oidc/{provider}/callback', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$provider = $args['provider'] ?? '';
+	$params = $request->getQueryParams();
+	$code = $params['code'] ?? null;
+	$state = $params['state'] ?? null;
+	$error = $params['error'] ?? null;
+	$errorDescription = $params['error_description'] ?? 'Unknown error';
+	// Handle error from provider
+	if ($error) {
+		$Organizr->outputOIDCCallbackError($errorDescription);
+		return $response;
+	}
+	// Validate required parameters
+	if (!$code || !$state) {
+		$Organizr->outputOIDCCallbackError('Missing code or state parameter');
+		return $response;
+	}
+	// Process callback
+	$user = $Organizr->processOIDCCallback($provider, $code, $state);
+	if ($user) {
+		$Organizr->outputOIDCCallbackSuccess($user['username']);
+	} else {
+		$Organizr->outputOIDCCallbackError($GLOBALS['api']['response']['message'] ?? 'Authentication failed');
+	}
+	return $response;
+});
+
+/**
+ * Test OIDC provider connection (admin only)
+ */
+$app->get('/oidc/{provider}/test', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->checkRoute($request)) {
+		if ($Organizr->qualifyRequest(1, true)) {
+			$provider = $args['provider'] ?? '';
+			$Organizr->testOIDCConnection($provider);
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});

+ 0 - 8
api/vendor/Expectation.php

@@ -1,8 +0,0 @@
-<?php
-
-namespace Pest;
-
-/**
-
- */
-final class Expectation {}

+ 12 - 2
api/vendor/autoload.php

@@ -3,8 +3,18 @@
 // autoload.php @generated by Composer
 
 if (PHP_VERSION_ID < 50600) {
-    echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
-    exit(1);
+    if (!headers_sent()) {
+        header('HTTP/1.1 500 Internal Server Error');
+    }
+    $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+    if (!ini_get('display_errors')) {
+        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+            fwrite(STDERR, $err);
+        } elseif (!headers_sent()) {
+            echo $err;
+        }
+    }
+    throw new RuntimeException($err);
 }
 
 require_once __DIR__ . '/composer/autoload_real.php';

+ 72 - 65
api/vendor/composer/ClassLoader.php

@@ -42,35 +42,37 @@ namespace Composer\Autoload;
  */
 class ClassLoader
 {
-    /** @var ?string */
+    /** @var \Closure(string):void */
+    private static $includeFile;
+
+    /** @var string|null */
     private $vendorDir;
 
     // PSR-4
     /**
-     * @var array[]
-     * @psalm-var array<string, array<string, int>>
+     * @var array<string, array<string, int>>
      */
     private $prefixLengthsPsr4 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, array<int, string>>
+     * @var array<string, list<string>>
      */
     private $prefixDirsPsr4 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, string>
+     * @var list<string>
      */
     private $fallbackDirsPsr4 = array();
 
     // PSR-0
     /**
-     * @var array[]
-     * @psalm-var array<string, array<string, string[]>>
+     * List of PSR-0 prefixes
+     *
+     * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
+     *
+     * @var array<string, array<string, list<string>>>
      */
     private $prefixesPsr0 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, string>
+     * @var list<string>
      */
     private $fallbackDirsPsr0 = array();
 
@@ -78,8 +80,7 @@ class ClassLoader
     private $useIncludePath = false;
 
     /**
-     * @var string[]
-     * @psalm-var array<string, string>
+     * @var array<string, string>
      */
     private $classMap = array();
 
@@ -87,29 +88,29 @@ class ClassLoader
     private $classMapAuthoritative = false;
 
     /**
-     * @var bool[]
-     * @psalm-var array<string, bool>
+     * @var array<string, bool>
      */
     private $missingClasses = array();
 
-    /** @var ?string */
+    /** @var string|null */
     private $apcuPrefix;
 
     /**
-     * @var self[]
+     * @var array<string, self>
      */
     private static $registeredLoaders = array();
 
     /**
-     * @param ?string $vendorDir
+     * @param string|null $vendorDir
      */
     public function __construct($vendorDir = null)
     {
         $this->vendorDir = $vendorDir;
+        self::initializeIncludeClosure();
     }
 
     /**
-     * @return string[]
+     * @return array<string, list<string>>
      */
     public function getPrefixes()
     {
@@ -121,8 +122,7 @@ class ClassLoader
     }
 
     /**
-     * @return array[]
-     * @psalm-return array<string, array<int, string>>
+     * @return array<string, list<string>>
      */
     public function getPrefixesPsr4()
     {
@@ -130,8 +130,7 @@ class ClassLoader
     }
 
     /**
-     * @return array[]
-     * @psalm-return array<string, string>
+     * @return list<string>
      */
     public function getFallbackDirs()
     {
@@ -139,8 +138,7 @@ class ClassLoader
     }
 
     /**
-     * @return array[]
-     * @psalm-return array<string, string>
+     * @return list<string>
      */
     public function getFallbackDirsPsr4()
     {
@@ -148,8 +146,7 @@ class ClassLoader
     }
 
     /**
-     * @return string[] Array of classname => path
-     * @psalm-return array<string, string>
+     * @return array<string, string> Array of classname => path
      */
     public function getClassMap()
     {
@@ -157,8 +154,7 @@ class ClassLoader
     }
 
     /**
-     * @param string[] $classMap Class to filename map
-     * @psalm-param array<string, string> $classMap
+     * @param array<string, string> $classMap Class to filename map
      *
      * @return void
      */
@@ -175,24 +171,25 @@ class ClassLoader
      * Registers a set of PSR-0 directories for a given prefix, either
      * appending or prepending to the ones previously set for this prefix.
      *
-     * @param string          $prefix  The prefix
-     * @param string[]|string $paths   The PSR-0 root directories
-     * @param bool            $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix
+     * @param list<string>|string $paths   The PSR-0 root directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @return void
      */
     public function add($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             if ($prepend) {
                 $this->fallbackDirsPsr0 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr0
                 );
             } else {
                 $this->fallbackDirsPsr0 = array_merge(
                     $this->fallbackDirsPsr0,
-                    (array) $paths
+                    $paths
                 );
             }
 
@@ -201,19 +198,19 @@ class ClassLoader
 
         $first = $prefix[0];
         if (!isset($this->prefixesPsr0[$first][$prefix])) {
-            $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+            $this->prefixesPsr0[$first][$prefix] = $paths;
 
             return;
         }
         if ($prepend) {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixesPsr0[$first][$prefix]
             );
         } else {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
                 $this->prefixesPsr0[$first][$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -222,9 +219,9 @@ class ClassLoader
      * Registers a set of PSR-4 directories for a given namespace, either
      * appending or prepending to the ones previously set for this namespace.
      *
-     * @param string          $prefix  The prefix/namespace, with trailing '\\'
-     * @param string[]|string $paths   The PSR-4 base directories
-     * @param bool            $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix/namespace, with trailing '\\'
+     * @param list<string>|string $paths   The PSR-4 base directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @throws \InvalidArgumentException
      *
@@ -232,17 +229,18 @@ class ClassLoader
      */
     public function addPsr4($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             // Register directories for the root namespace.
             if ($prepend) {
                 $this->fallbackDirsPsr4 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr4
                 );
             } else {
                 $this->fallbackDirsPsr4 = array_merge(
                     $this->fallbackDirsPsr4,
-                    (array) $paths
+                    $paths
                 );
             }
         } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
@@ -252,18 +250,18 @@ class ClassLoader
                 throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
             }
             $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
-            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+            $this->prefixDirsPsr4[$prefix] = $paths;
         } elseif ($prepend) {
             // Prepend directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixDirsPsr4[$prefix]
             );
         } else {
             // Append directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
                 $this->prefixDirsPsr4[$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -272,8 +270,8 @@ class ClassLoader
      * Registers a set of PSR-0 directories for a given prefix,
      * replacing any others previously set for this prefix.
      *
-     * @param string          $prefix The prefix
-     * @param string[]|string $paths  The PSR-0 base directories
+     * @param string              $prefix The prefix
+     * @param list<string>|string $paths  The PSR-0 base directories
      *
      * @return void
      */
@@ -290,8 +288,8 @@ class ClassLoader
      * Registers a set of PSR-4 directories for a given namespace,
      * replacing any others previously set for this namespace.
      *
-     * @param string          $prefix The prefix/namespace, with trailing '\\'
-     * @param string[]|string $paths  The PSR-4 base directories
+     * @param string              $prefix The prefix/namespace, with trailing '\\'
+     * @param list<string>|string $paths  The PSR-4 base directories
      *
      * @throws \InvalidArgumentException
      *
@@ -425,7 +423,8 @@ class ClassLoader
     public function loadClass($class)
     {
         if ($file = $this->findFile($class)) {
-            includeFile($file);
+            $includeFile = self::$includeFile;
+            $includeFile($file);
 
             return true;
         }
@@ -476,9 +475,9 @@ class ClassLoader
     }
 
     /**
-     * Returns the currently registered loaders indexed by their corresponding vendor directories.
+     * Returns the currently registered loaders keyed by their corresponding vendor directories.
      *
-     * @return self[]
+     * @return array<string, self>
      */
     public static function getRegisteredLoaders()
     {
@@ -555,18 +554,26 @@ class ClassLoader
 
         return false;
     }
-}
 
-/**
- * Scope isolated include.
- *
- * Prevents access to $this/self from included files.
- *
- * @param  string $file
- * @return void
- * @private
- */
-function includeFile($file)
-{
-    include $file;
+    /**
+     * @return void
+     */
+    private static function initializeIncludeClosure()
+    {
+        if (self::$includeFile !== null) {
+            return;
+        }
+
+        /**
+         * Scope isolated include.
+         *
+         * Prevents access to $this/self from included files.
+         *
+         * @param  string $file
+         * @return void
+         */
+        self::$includeFile = \Closure::bind(static function($file) {
+            include $file;
+        }, null, null);
+    }
 }

+ 58 - 14
api/vendor/composer/InstalledVersions.php

@@ -26,12 +26,23 @@ use Composer\Semver\VersionParser;
  */
 class InstalledVersions
 {
+    /**
+     * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
+     * @internal
+     */
+    private static $selfDir = null;
+
     /**
      * @var mixed[]|null
-     * @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}|array{}|null
+     * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
      */
     private static $installed;
 
+    /**
+     * @var bool
+     */
+    private static $installedIsLocalDir;
+
     /**
      * @var bool|null
      */
@@ -39,7 +50,7 @@ class InstalledVersions
 
     /**
      * @var array[]
-     * @psalm-var array<string, array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
+     * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
      */
     private static $installedByVendor = array();
 
@@ -98,7 +109,7 @@ class InstalledVersions
     {
         foreach (self::getInstalled() as $installed) {
             if (isset($installed['versions'][$packageName])) {
-                return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
+                return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
             }
         }
 
@@ -119,7 +130,7 @@ class InstalledVersions
      */
     public static function satisfies(VersionParser $parser, $packageName, $constraint)
     {
-        $constraint = $parser->parseConstraints($constraint);
+        $constraint = $parser->parseConstraints((string) $constraint);
         $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
 
         return $provided->matches($constraint);
@@ -243,7 +254,7 @@ class InstalledVersions
 
     /**
      * @return array
-     * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}
+     * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
      */
     public static function getRootPackage()
     {
@@ -257,7 +268,7 @@ class InstalledVersions
      *
      * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
      * @return array[]
-     * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}
+     * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
      */
     public static function getRawData()
     {
@@ -280,7 +291,7 @@ class InstalledVersions
      * Returns the raw data of all installed.php which are currently loaded for custom implementations
      *
      * @return array[]
-     * @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
+     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
      */
     public static function getAllRawData()
     {
@@ -303,17 +314,35 @@ class InstalledVersions
      * @param  array[] $data A vendor/composer/installed.php data set
      * @return void
      *
-     * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>} $data
+     * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
      */
     public static function reload($data)
     {
         self::$installed = $data;
         self::$installedByVendor = array();
+
+        // when using reload, we disable the duplicate protection to ensure that self::$installed data is
+        // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
+        // so we have to assume it does not, and that may result in duplicate data being returned when listing
+        // all installed packages for example
+        self::$installedIsLocalDir = false;
+    }
+
+    /**
+     * @return string
+     */
+    private static function getSelfDir()
+    {
+        if (self::$selfDir === null) {
+            self::$selfDir = strtr(__DIR__, '\\', '/');
+        }
+
+        return self::$selfDir;
     }
 
     /**
      * @return array[]
-     * @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
+     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
      */
     private static function getInstalled()
     {
@@ -322,17 +351,27 @@ class InstalledVersions
         }
 
         $installed = array();
+        $copiedLocalDir = false;
 
         if (self::$canGetVendors) {
+            $selfDir = self::getSelfDir();
             foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+                $vendorDir = strtr($vendorDir, '\\', '/');
                 if (isset(self::$installedByVendor[$vendorDir])) {
                     $installed[] = self::$installedByVendor[$vendorDir];
                 } elseif (is_file($vendorDir.'/composer/installed.php')) {
-                    $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
-                    if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
-                        self::$installed = $installed[count($installed) - 1];
+                    /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                    $required = require $vendorDir.'/composer/installed.php';
+                    self::$installedByVendor[$vendorDir] = $required;
+                    $installed[] = $required;
+                    if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
+                        self::$installed = $required;
+                        self::$installedIsLocalDir = true;
                     }
                 }
+                if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
+                    $copiedLocalDir = true;
+                }
             }
         }
 
@@ -340,12 +379,17 @@ class InstalledVersions
             // only require the installed.php file if this file is loaded from its dumped location,
             // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
             if (substr(__DIR__, -8, 1) !== 'C') {
-                self::$installed = require __DIR__ . '/installed.php';
+                /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                $required = require __DIR__ . '/installed.php';
+                self::$installed = $required;
             } else {
                 self::$installed = array();
             }
         }
-        $installed[] = self::$installed;
+
+        if (self::$installed !== array() && !$copiedLocalDir) {
+            $installed[] = self::$installed;
+        }
 
         return $installed;
     }

+ 1 - 1
api/vendor/composer/autoload_psr4.php

@@ -27,7 +27,7 @@ return array(
     'Pusher\\' => array($vendorDir . '/pusher/pusher-php-server/src'),
     'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
     'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
-    'Psr\\Http\\Server\\' => array($vendorDir . '/psr/http-server-handler/src', $vendorDir . '/psr/http-server-middleware/src'),
+    'Psr\\Http\\Server\\' => array($vendorDir . '/psr/http-server-middleware/src', $vendorDir . '/psr/http-server-handler/src'),
     'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
     'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
     'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),

+ 10 - 17
api/vendor/composer/autoload_real.php

@@ -33,25 +33,18 @@ class ComposerAutoloaderInitcbdc783d76f8e7563dcce7d8af053ecb
 
         $loader->register(true);
 
-        $includeFiles = \Composer\Autoload\ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb::$files;
-        foreach ($includeFiles as $fileIdentifier => $file) {
-            composerRequirecbdc783d76f8e7563dcce7d8af053ecb($fileIdentifier, $file);
+        $filesToLoad = \Composer\Autoload\ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb::$files;
+        $requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
+            if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+                $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
+
+                require $file;
+            }
+        }, null, null);
+        foreach ($filesToLoad as $fileIdentifier => $file) {
+            $requireFile($fileIdentifier, $file);
         }
 
         return $loader;
     }
 }
-
-/**
- * @param string $fileIdentifier
- * @param string $file
- * @return void
- */
-function composerRequirecbdc783d76f8e7563dcce7d8af053ecb($fileIdentifier, $file)
-{
-    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
-        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
-
-        require $file;
-    }
-}

+ 82 - 82
api/vendor/composer/autoload_static.php

@@ -28,15 +28,15 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
     );
 
     public static $prefixLengthsPsr4 = array (
-        'W' => 
+        'W' =>
         array (
             'Webmozart\\Assert\\' => 17,
         ),
-        'T' => 
+        'T' =>
         array (
             'Tightenco\\Collect\\' => 18,
         ),
-        'S' => 
+        'S' =>
         array (
             'Symfony\\Polyfill\\Util\\' => 22,
             'Symfony\\Polyfill\\Php81\\' => 23,
@@ -52,13 +52,13 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
             'Slim\\Psr7\\' => 10,
             'Slim\\' => 5,
         ),
-        'R' => 
+        'R' =>
         array (
             'Recurr\\' => 7,
             'Ramsey\\Uuid\\' => 12,
             'Ramsey\\Collection\\' => 18,
         ),
-        'P' => 
+        'P' =>
         array (
             'Pusher\\' => 7,
             'Psr\\SimpleCache\\' => 16,
@@ -73,323 +73,323 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
             'PHPMailer\\PHPMailer\\' => 20,
             'PHPHtmlParser\\' => 14,
         ),
-        'O' => 
+        'O' =>
         array (
             'OpenApi\\' => 8,
         ),
-        'N' => 
+        'N' =>
         array (
             'Nekonomokochan\\PhpJsonLogger\\' => 29,
         ),
-        'M' => 
+        'M' =>
         array (
             'MyCLabs\\Enum\\' => 13,
             'Monolog\\' => 8,
         ),
-        'L' => 
+        'L' =>
         array (
             'League\\OAuth2\\Client\\' => 21,
             'Lcobucci\\JWT\\' => 13,
             'Lcobucci\\Clock\\' => 15,
         ),
-        'K' => 
+        'K' =>
         array (
             'Kryptonit3\\Sonarr\\' => 18,
             'Kryptonit3\\SickRage\\' => 20,
             'Kryptonit3\\CouchPotato\\' => 23,
         ),
-        'I' => 
+        'I' =>
         array (
             'Illuminate\\Contracts\\' => 21,
         ),
-        'H' => 
+        'H' =>
         array (
             'Http\\Promise\\' => 13,
             'Http\\Client\\' => 12,
         ),
-        'G' => 
+        'G' =>
         array (
             'GuzzleHttp\\Psr7\\' => 16,
             'GuzzleHttp\\Promise\\' => 19,
             'GuzzleHttp\\' => 11,
             'GO\\' => 3,
         ),
-        'F' => 
+        'F' =>
         array (
             'Fig\\Http\\Message\\' => 17,
             'FastRoute\\' => 10,
         ),
-        'D' => 
+        'D' =>
         array (
             'Doctrine\\Common\\Lexer\\' => 22,
             'Doctrine\\Common\\Collections\\' => 28,
             'Doctrine\\Common\\Annotations\\' => 28,
         ),
-        'C' => 
+        'C' =>
         array (
             'Cron\\' => 5,
             'Composer\\Semver\\' => 16,
         ),
-        'B' => 
+        'B' =>
         array (
             'Brick\\Math\\' => 11,
             'Bogstag\\OAuth2\\Client\\' => 22,
             'Bcremer\\LineReader\\' => 19,
         ),
-        'A' => 
+        'A' =>
         array (
             'Adldap\\' => 7,
         ),
     );
 
     public static $prefixDirsPsr4 = array (
-        'Webmozart\\Assert\\' => 
+        'Webmozart\\Assert\\' =>
         array (
             0 => __DIR__ . '/..' . '/webmozart/assert/src',
         ),
-        'Tightenco\\Collect\\' => 
+        'Tightenco\\Collect\\' =>
         array (
             0 => __DIR__ . '/..' . '/tightenco/collect/src/Collect',
         ),
-        'Symfony\\Polyfill\\Util\\' => 
+        'Symfony\\Polyfill\\Util\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-util',
         ),
-        'Symfony\\Polyfill\\Php81\\' => 
+        'Symfony\\Polyfill\\Php81\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-php81',
         ),
-        'Symfony\\Polyfill\\Php80\\' => 
+        'Symfony\\Polyfill\\Php80\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
         ),
-        'Symfony\\Polyfill\\Php72\\' => 
+        'Symfony\\Polyfill\\Php72\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-php72',
         ),
-        'Symfony\\Polyfill\\Php56\\' => 
+        'Symfony\\Polyfill\\Php56\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-php56',
         ),
-        'Symfony\\Polyfill\\Mbstring\\' => 
+        'Symfony\\Polyfill\\Mbstring\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
         ),
-        'Symfony\\Polyfill\\Ctype\\' => 
+        'Symfony\\Polyfill\\Ctype\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
         ),
-        'Symfony\\Component\\Yaml\\' => 
+        'Symfony\\Component\\Yaml\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/yaml',
         ),
-        'Symfony\\Component\\VarDumper\\' => 
+        'Symfony\\Component\\VarDumper\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/var-dumper',
         ),
-        'Symfony\\Component\\Finder\\' => 
+        'Symfony\\Component\\Finder\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/finder',
         ),
-        'Stripe\\' => 
+        'Stripe\\' =>
         array (
             0 => __DIR__ . '/..' . '/stripe/stripe-php/lib',
         ),
-        'Slim\\Psr7\\' => 
+        'Slim\\Psr7\\' =>
         array (
             0 => __DIR__ . '/..' . '/slim/psr7/src',
         ),
-        'Slim\\' => 
+        'Slim\\' =>
         array (
             0 => __DIR__ . '/..' . '/slim/slim/Slim',
         ),
-        'Recurr\\' => 
+        'Recurr\\' =>
         array (
             0 => __DIR__ . '/..' . '/simshaun/recurr/src/Recurr',
         ),
-        'Ramsey\\Uuid\\' => 
+        'Ramsey\\Uuid\\' =>
         array (
             0 => __DIR__ . '/..' . '/ramsey/uuid/src',
         ),
-        'Ramsey\\Collection\\' => 
+        'Ramsey\\Collection\\' =>
         array (
             0 => __DIR__ . '/..' . '/ramsey/collection/src',
         ),
-        'Pusher\\' => 
+        'Pusher\\' =>
         array (
             0 => __DIR__ . '/..' . '/pusher/pusher-php-server/src',
         ),
-        'Psr\\SimpleCache\\' => 
+        'Psr\\SimpleCache\\' =>
         array (
             0 => __DIR__ . '/..' . '/psr/simple-cache/src',
         ),
-        'Psr\\Log\\' => 
+        'Psr\\Log\\' =>
         array (
             0 => __DIR__ . '/..' . '/psr/log/Psr/Log',
         ),
-        'Psr\\Http\\Server\\' => 
+        'Psr\\Http\\Server\\' =>
         array (
-            0 => __DIR__ . '/..' . '/psr/http-server-handler/src',
-            1 => __DIR__ . '/..' . '/psr/http-server-middleware/src',
+            0 => __DIR__ . '/..' . '/psr/http-server-middleware/src',
+            1 => __DIR__ . '/..' . '/psr/http-server-handler/src',
         ),
-        'Psr\\Http\\Message\\' => 
+        'Psr\\Http\\Message\\' =>
         array (
             0 => __DIR__ . '/..' . '/psr/http-factory/src',
             1 => __DIR__ . '/..' . '/psr/http-message/src',
         ),
-        'Psr\\Http\\Client\\' => 
+        'Psr\\Http\\Client\\' =>
         array (
             0 => __DIR__ . '/..' . '/psr/http-client/src',
         ),
-        'Psr\\Container\\' => 
+        'Psr\\Container\\' =>
         array (
             0 => __DIR__ . '/..' . '/psr/container/src',
         ),
-        'PragmaRX\\Google2FA\\Tests\\' => 
+        'PragmaRX\\Google2FA\\Tests\\' =>
         array (
             0 => __DIR__ . '/..' . '/pragmarx/google2fa/tests',
         ),
-        'PragmaRX\\Google2FA\\' => 
+        'PragmaRX\\Google2FA\\' =>
         array (
             0 => __DIR__ . '/..' . '/pragmarx/google2fa/src',
         ),
-        'ParagonIE\\ConstantTime\\' => 
+        'ParagonIE\\ConstantTime\\' =>
         array (
             0 => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src',
         ),
-        'PHPMailer\\PHPMailer\\' => 
+        'PHPMailer\\PHPMailer\\' =>
         array (
             0 => __DIR__ . '/..' . '/phpmailer/phpmailer/src',
         ),
-        'PHPHtmlParser\\' => 
+        'PHPHtmlParser\\' =>
         array (
             0 => __DIR__ . '/..' . '/paquettg/php-html-parser/src/PHPHtmlParser',
         ),
-        'OpenApi\\' => 
+        'OpenApi\\' =>
         array (
             0 => __DIR__ . '/..' . '/zircote/swagger-php/src',
         ),
-        'Nekonomokochan\\PhpJsonLogger\\' => 
+        'Nekonomokochan\\PhpJsonLogger\\' =>
         array (
             0 => __DIR__ . '/..' . '/nekonomokochan/php-json-logger/src/PhpJsonLogger',
         ),
-        'MyCLabs\\Enum\\' => 
+        'MyCLabs\\Enum\\' =>
         array (
             0 => __DIR__ . '/..' . '/myclabs/php-enum/src',
         ),
-        'Monolog\\' => 
+        'Monolog\\' =>
         array (
             0 => __DIR__ . '/..' . '/monolog/monolog/src/Monolog',
         ),
-        'League\\OAuth2\\Client\\' => 
+        'League\\OAuth2\\Client\\' =>
         array (
             0 => __DIR__ . '/..' . '/league/oauth2-client/src',
         ),
-        'Lcobucci\\JWT\\' => 
+        'Lcobucci\\JWT\\' =>
         array (
             0 => __DIR__ . '/..' . '/lcobucci/jwt/src',
         ),
-        'Lcobucci\\Clock\\' => 
+        'Lcobucci\\Clock\\' =>
         array (
             0 => __DIR__ . '/..' . '/lcobucci/clock/src',
         ),
-        'Kryptonit3\\Sonarr\\' => 
+        'Kryptonit3\\Sonarr\\' =>
         array (
             0 => __DIR__ . '/..' . '/kryptonit3/sonarr/src',
         ),
-        'Kryptonit3\\SickRage\\' => 
+        'Kryptonit3\\SickRage\\' =>
         array (
             0 => __DIR__ . '/..' . '/kryptonit3/sickrage/src',
         ),
-        'Kryptonit3\\CouchPotato\\' => 
+        'Kryptonit3\\CouchPotato\\' =>
         array (
             0 => __DIR__ . '/..' . '/kryptonit3/couchpotato/src',
         ),
-        'Illuminate\\Contracts\\' => 
+        'Illuminate\\Contracts\\' =>
         array (
             0 => __DIR__ . '/..' . '/illuminate/contracts',
         ),
-        'Http\\Promise\\' => 
+        'Http\\Promise\\' =>
         array (
             0 => __DIR__ . '/..' . '/php-http/promise/src',
         ),
-        'Http\\Client\\' => 
+        'Http\\Client\\' =>
         array (
             0 => __DIR__ . '/..' . '/php-http/httplug/src',
         ),
-        'GuzzleHttp\\Psr7\\' => 
+        'GuzzleHttp\\Psr7\\' =>
         array (
             0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
         ),
-        'GuzzleHttp\\Promise\\' => 
+        'GuzzleHttp\\Promise\\' =>
         array (
             0 => __DIR__ . '/..' . '/guzzlehttp/promises/src',
         ),
-        'GuzzleHttp\\' => 
+        'GuzzleHttp\\' =>
         array (
             0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
         ),
-        'GO\\' => 
+        'GO\\' =>
         array (
             0 => __DIR__ . '/..' . '/peppeocchi/php-cron-scheduler/src/GO',
         ),
-        'Fig\\Http\\Message\\' => 
+        'Fig\\Http\\Message\\' =>
         array (
             0 => __DIR__ . '/..' . '/fig/http-message-util/src',
         ),
-        'FastRoute\\' => 
+        'FastRoute\\' =>
         array (
             0 => __DIR__ . '/..' . '/nikic/fast-route/src',
         ),
-        'Doctrine\\Common\\Lexer\\' => 
+        'Doctrine\\Common\\Lexer\\' =>
         array (
             0 => __DIR__ . '/..' . '/doctrine/lexer/lib/Doctrine/Common/Lexer',
         ),
-        'Doctrine\\Common\\Collections\\' => 
+        'Doctrine\\Common\\Collections\\' =>
         array (
             0 => __DIR__ . '/..' . '/doctrine/collections/lib/Doctrine/Common/Collections',
         ),
-        'Doctrine\\Common\\Annotations\\' => 
+        'Doctrine\\Common\\Annotations\\' =>
         array (
             0 => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations',
         ),
-        'Cron\\' => 
+        'Cron\\' =>
         array (
             0 => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron',
         ),
-        'Composer\\Semver\\' => 
+        'Composer\\Semver\\' =>
         array (
             0 => __DIR__ . '/..' . '/composer/semver/src',
         ),
-        'Brick\\Math\\' => 
+        'Brick\\Math\\' =>
         array (
             0 => __DIR__ . '/..' . '/brick/math/src',
         ),
-        'Bogstag\\OAuth2\\Client\\' => 
+        'Bogstag\\OAuth2\\Client\\' =>
         array (
             0 => __DIR__ . '/..' . '/bogstag/oauth2-trakt/src',
         ),
-        'Bcremer\\LineReader\\' => 
+        'Bcremer\\LineReader\\' =>
         array (
             0 => __DIR__ . '/..' . '/bcremer/line-reader/src',
         ),
-        'Adldap\\' => 
+        'Adldap\\' =>
         array (
             0 => __DIR__ . '/..' . '/adldap2/adldap2/src',
         ),
     );
 
     public static $prefixesPsr0 = array (
-        's' => 
+        's' =>
         array (
-            'stringEncode' => 
+            'stringEncode' =>
             array (
                 0 => __DIR__ . '/..' . '/paquettg/string-encode/src',
             ),
         ),
-        'R' => 
+        'R' =>
         array (
-            'Requests' => 
+            'Requests' =>
             array (
                 0 => __DIR__ . '/..' . '/rmccue/requests/library',
             ),

+ 10 - 10
api/vendor/composer/installed.json

@@ -1269,17 +1269,17 @@
         },
         {
             "name": "league/oauth2-client",
-            "version": "2.6.0",
-            "version_normalized": "2.6.0.0",
+            "version": "2.7.0",
+            "version_normalized": "2.7.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/oauth2-client.git",
-                "reference": "badb01e62383430706433191b82506b6df24ad98"
+                "reference": "160d6274b03562ebeb55ed18399281d8118b76c8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/badb01e62383430706433191b82506b6df24ad98",
-                "reference": "badb01e62383430706433191b82506b6df24ad98",
+                "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/160d6274b03562ebeb55ed18399281d8118b76c8",
+                "reference": "160d6274b03562ebeb55ed18399281d8118b76c8",
                 "shasum": ""
             },
             "require": {
@@ -1288,12 +1288,12 @@
                 "php": "^5.6 || ^7.0 || ^8.0"
             },
             "require-dev": {
-                "mockery/mockery": "^1.3",
-                "php-parallel-lint/php-parallel-lint": "^1.2",
-                "phpunit/phpunit": "^5.7 || ^6.0 || ^9.3",
+                "mockery/mockery": "^1.3.5",
+                "php-parallel-lint/php-parallel-lint": "^1.3.1",
+                "phpunit/phpunit": "^5.7 || ^6.0 || ^9.5",
                 "squizlabs/php_codesniffer": "^2.3 || ^3.0"
             },
-            "time": "2020-10-28T02:03:40+00:00",
+            "time": "2023-04-16T18:19:15+00:00",
             "type": "library",
             "extra": {
                 "branch-alias": {
@@ -1336,7 +1336,7 @@
             ],
             "support": {
                 "issues": "https://github.com/thephpleague/oauth2-client/issues",
-                "source": "https://github.com/thephpleague/oauth2-client/tree/2.6.0"
+                "source": "https://github.com/thephpleague/oauth2-client/tree/2.7.0"
             },
             "install-path": "../league/oauth2-client"
         },

+ 71 - 71
api/vendor/composer/installed.php

@@ -1,67 +1,67 @@
 <?php return array(
     'root' => array(
+        'name' => '__root__',
         'pretty_version' => 'dev-master',
         'version' => 'dev-master',
+        'reference' => '3ecd8fb1e5f285e2ac49f681c82131b6b08bac50',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
-        'reference' => '1ae02fda50382bb86ea50b76579b5e773c4a1ff1',
-        'name' => '__root__',
         'dev' => true,
     ),
     'versions' => array(
         '__root__' => array(
             'pretty_version' => 'dev-master',
             'version' => 'dev-master',
+            'reference' => '3ecd8fb1e5f285e2ac49f681c82131b6b08bac50',
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
-            'reference' => '1ae02fda50382bb86ea50b76579b5e773c4a1ff1',
             'dev_requirement' => false,
         ),
         'adldap2/adldap2' => array(
             'pretty_version' => 'v10.4.1',
             'version' => '10.4.1.0',
+            'reference' => '81aeb283f56e216ae925a9cc4241de56b1fd4453',
             'type' => 'library',
             'install_path' => __DIR__ . '/../adldap2/adldap2',
             'aliases' => array(),
-            'reference' => '81aeb283f56e216ae925a9cc4241de56b1fd4453',
             'dev_requirement' => false,
         ),
         'bcremer/line-reader' => array(
             'pretty_version' => '1.1.0',
             'version' => '1.1.0.0',
+            'reference' => '3ec3e200577630f1e58d30b4c1c468b877d8d0a7',
             'type' => 'library',
             'install_path' => __DIR__ . '/../bcremer/line-reader',
             'aliases' => array(),
-            'reference' => '3ec3e200577630f1e58d30b4c1c468b877d8d0a7',
             'dev_requirement' => false,
         ),
         'bogstag/oauth2-trakt' => array(
             'pretty_version' => 'v1.0.1',
             'version' => '1.0.1.0',
+            'reference' => 'fbb9253d9e317e84dc2b3f1253afc1dcbb4414a2',
             'type' => 'library',
             'install_path' => __DIR__ . '/../bogstag/oauth2-trakt',
             'aliases' => array(),
-            'reference' => 'fbb9253d9e317e84dc2b3f1253afc1dcbb4414a2',
             'dev_requirement' => false,
         ),
         'brick/math' => array(
             'pretty_version' => '0.9.3',
             'version' => '0.9.3.0',
+            'reference' => 'ca57d18f028f84f777b2168cd1911b0dee2343ae',
             'type' => 'library',
             'install_path' => __DIR__ . '/../brick/math',
             'aliases' => array(),
-            'reference' => 'ca57d18f028f84f777b2168cd1911b0dee2343ae',
             'dev_requirement' => false,
         ),
         'composer/semver' => array(
             'pretty_version' => '1.7.2',
             'version' => '1.7.2.0',
+            'reference' => '647490bbcaf7fc4891c58f47b825eb99d19c377a',
             'type' => 'library',
             'install_path' => __DIR__ . '/./semver',
             'aliases' => array(),
-            'reference' => '647490bbcaf7fc4891c58f47b825eb99d19c377a',
             'dev_requirement' => false,
         ),
         'dg/dibi' => array(
@@ -73,154 +73,154 @@
         'dibi/dibi' => array(
             'pretty_version' => 'v4.2.6',
             'version' => '4.2.6.0',
+            'reference' => '9d5d430d3d04ad8aff0e1570390e9cfbb7f3c538',
             'type' => 'library',
             'install_path' => __DIR__ . '/../dibi/dibi',
             'aliases' => array(),
-            'reference' => '9d5d430d3d04ad8aff0e1570390e9cfbb7f3c538',
             'dev_requirement' => false,
         ),
         'doctrine/annotations' => array(
             'pretty_version' => '1.10.3',
             'version' => '1.10.3.0',
+            'reference' => '5db60a4969eba0e0c197a19c077780aadbc43c5d',
             'type' => 'library',
             'install_path' => __DIR__ . '/../doctrine/annotations',
             'aliases' => array(),
-            'reference' => '5db60a4969eba0e0c197a19c077780aadbc43c5d',
             'dev_requirement' => false,
         ),
         'doctrine/collections' => array(
             'pretty_version' => '1.6.8',
             'version' => '1.6.8.0',
+            'reference' => '1958a744696c6bb3bb0d28db2611dc11610e78af',
             'type' => 'library',
             'install_path' => __DIR__ . '/../doctrine/collections',
             'aliases' => array(),
-            'reference' => '1958a744696c6bb3bb0d28db2611dc11610e78af',
             'dev_requirement' => false,
         ),
         'doctrine/lexer' => array(
             'pretty_version' => '1.2.1',
             'version' => '1.2.1.0',
+            'reference' => 'e864bbf5904cb8f5bb334f99209b48018522f042',
             'type' => 'library',
             'install_path' => __DIR__ . '/../doctrine/lexer',
             'aliases' => array(),
-            'reference' => 'e864bbf5904cb8f5bb334f99209b48018522f042',
             'dev_requirement' => false,
         ),
         'dragonmantank/cron-expression' => array(
             'pretty_version' => 'v3.1.0',
             'version' => '3.1.0.0',
+            'reference' => '7a8c6e56ab3ffcc538d05e8155bb42269abf1a0c',
             'type' => 'library',
             'install_path' => __DIR__ . '/../dragonmantank/cron-expression',
             'aliases' => array(),
-            'reference' => '7a8c6e56ab3ffcc538d05e8155bb42269abf1a0c',
             'dev_requirement' => false,
         ),
         'fig/http-message-util' => array(
             'pretty_version' => '1.1.4',
             'version' => '1.1.4.0',
+            'reference' => '3242caa9da7221a304b8f84eb9eaddae0a7cf422',
             'type' => 'library',
             'install_path' => __DIR__ . '/../fig/http-message-util',
             'aliases' => array(),
-            'reference' => '3242caa9da7221a304b8f84eb9eaddae0a7cf422',
             'dev_requirement' => false,
         ),
         'guzzlehttp/guzzle' => array(
             'pretty_version' => '7.3.0',
             'version' => '7.3.0.0',
+            'reference' => '7008573787b430c1c1f650e3722d9bba59967628',
             'type' => 'library',
             'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
             'aliases' => array(),
-            'reference' => '7008573787b430c1c1f650e3722d9bba59967628',
             'dev_requirement' => false,
         ),
         'guzzlehttp/promises' => array(
             'pretty_version' => '1.4.1',
             'version' => '1.4.1.0',
+            'reference' => '8e7d04f1f6450fef59366c399cfad4b9383aa30d',
             'type' => 'library',
             'install_path' => __DIR__ . '/../guzzlehttp/promises',
             'aliases' => array(),
-            'reference' => '8e7d04f1f6450fef59366c399cfad4b9383aa30d',
             'dev_requirement' => false,
         ),
         'guzzlehttp/psr7' => array(
             'pretty_version' => '1.8.2',
             'version' => '1.8.2.0',
+            'reference' => 'dc960a912984efb74d0a90222870c72c87f10c91',
             'type' => 'library',
             'install_path' => __DIR__ . '/../guzzlehttp/psr7',
             'aliases' => array(),
-            'reference' => 'dc960a912984efb74d0a90222870c72c87f10c91',
             'dev_requirement' => false,
         ),
         'illuminate/contracts' => array(
             'pretty_version' => 'v5.8.0',
             'version' => '5.8.0.0',
+            'reference' => '3e3a9a654adbf798e05491a5dbf90112df1effde',
             'type' => 'library',
             'install_path' => __DIR__ . '/../illuminate/contracts',
             'aliases' => array(),
-            'reference' => '3e3a9a654adbf798e05491a5dbf90112df1effde',
             'dev_requirement' => false,
         ),
         'kryptonit3/couchpotato' => array(
             'pretty_version' => '1.0.0',
             'version' => '1.0.0.0',
+            'reference' => '7a1fc892f70f120f74ff005850e923a0f1566376',
             'type' => 'library',
             'install_path' => __DIR__ . '/../kryptonit3/couchpotato',
             'aliases' => array(),
-            'reference' => '7a1fc892f70f120f74ff005850e923a0f1566376',
             'dev_requirement' => false,
         ),
         'kryptonit3/sickrage' => array(
             'pretty_version' => '1.0.1',
             'version' => '1.0.1.0',
+            'reference' => '441a293b5c219c3cdd1ebebd2bcf4518598f84aa',
             'type' => 'library',
             'install_path' => __DIR__ . '/../kryptonit3/sickrage',
             'aliases' => array(),
-            'reference' => '441a293b5c219c3cdd1ebebd2bcf4518598f84aa',
             'dev_requirement' => false,
         ),
         'kryptonit3/sonarr' => array(
             'pretty_version' => '1.0.6.1',
             'version' => '1.0.6.1',
+            'reference' => 'e30c5c783a837270bcef81571ca9b95909c52e5e',
             'type' => 'library',
             'install_path' => __DIR__ . '/../kryptonit3/sonarr',
             'aliases' => array(),
-            'reference' => 'e30c5c783a837270bcef81571ca9b95909c52e5e',
             'dev_requirement' => false,
         ),
         'lcobucci/clock' => array(
             'pretty_version' => '2.0.0',
             'version' => '2.0.0.0',
+            'reference' => '353d83fe2e6ae95745b16b3d911813df6a05bfb3',
             'type' => 'library',
             'install_path' => __DIR__ . '/../lcobucci/clock',
             'aliases' => array(),
-            'reference' => '353d83fe2e6ae95745b16b3d911813df6a05bfb3',
             'dev_requirement' => false,
         ),
         'lcobucci/jwt' => array(
             'pretty_version' => '4.1.5',
             'version' => '4.1.5.0',
+            'reference' => 'fe2d89f2eaa7087af4aa166c6f480ef04e000582',
             'type' => 'library',
             'install_path' => __DIR__ . '/../lcobucci/jwt',
             'aliases' => array(),
-            'reference' => 'fe2d89f2eaa7087af4aa166c6f480ef04e000582',
             'dev_requirement' => false,
         ),
         'league/oauth2-client' => array(
-            'pretty_version' => '2.6.0',
-            'version' => '2.6.0.0',
+            'pretty_version' => '2.7.0',
+            'version' => '2.7.0.0',
+            'reference' => '160d6274b03562ebeb55ed18399281d8118b76c8',
             'type' => 'library',
             'install_path' => __DIR__ . '/../league/oauth2-client',
             'aliases' => array(),
-            'reference' => 'badb01e62383430706433191b82506b6df24ad98',
             'dev_requirement' => false,
         ),
         'monolog/monolog' => array(
             'pretty_version' => '1.26.1',
             'version' => '1.26.1.0',
+            'reference' => 'c6b00f05152ae2c9b04a448f99c7590beb6042f5',
             'type' => 'library',
             'install_path' => __DIR__ . '/../monolog/monolog',
             'aliases' => array(),
-            'reference' => 'c6b00f05152ae2c9b04a448f99c7590beb6042f5',
             'dev_requirement' => false,
         ),
         'mtdowling/cron-expression' => array(
@@ -232,136 +232,136 @@
         'myclabs/php-enum' => array(
             'pretty_version' => '1.8.0',
             'version' => '1.8.0.0',
+            'reference' => '46cf3d8498b095bd33727b13fd5707263af99421',
             'type' => 'library',
             'install_path' => __DIR__ . '/../myclabs/php-enum',
             'aliases' => array(),
-            'reference' => '46cf3d8498b095bd33727b13fd5707263af99421',
             'dev_requirement' => false,
         ),
         'nekonomokochan/php-json-logger' => array(
             'pretty_version' => 'v1.3.1',
             'version' => '1.3.1.0',
+            'reference' => '6df126a82940a00d8ea2da6e0b7c58e3e57eb132',
             'type' => 'library',
             'install_path' => __DIR__ . '/../nekonomokochan/php-json-logger',
             'aliases' => array(),
-            'reference' => '6df126a82940a00d8ea2da6e0b7c58e3e57eb132',
             'dev_requirement' => false,
         ),
         'nikic/fast-route' => array(
             'pretty_version' => 'v1.3.0',
             'version' => '1.3.0.0',
+            'reference' => '181d480e08d9476e61381e04a71b34dc0432e812',
             'type' => 'library',
             'install_path' => __DIR__ . '/../nikic/fast-route',
             'aliases' => array(),
-            'reference' => '181d480e08d9476e61381e04a71b34dc0432e812',
             'dev_requirement' => false,
         ),
         'paquettg/php-html-parser' => array(
             'pretty_version' => '3.1.1',
             'version' => '3.1.1.0',
+            'reference' => '4e01a438ad5961cc2d7427eb9798d213c8a12629',
             'type' => 'library',
             'install_path' => __DIR__ . '/../paquettg/php-html-parser',
             'aliases' => array(),
-            'reference' => '4e01a438ad5961cc2d7427eb9798d213c8a12629',
             'dev_requirement' => false,
         ),
         'paquettg/string-encode' => array(
             'pretty_version' => '1.0.1',
             'version' => '1.0.1.0',
+            'reference' => 'a8708e9fac9d5ddfc8fc2aac6004e2cd05d80fee',
             'type' => 'library',
             'install_path' => __DIR__ . '/../paquettg/string-encode',
             'aliases' => array(),
-            'reference' => 'a8708e9fac9d5ddfc8fc2aac6004e2cd05d80fee',
             'dev_requirement' => false,
         ),
         'paragonie/constant_time_encoding' => array(
             'pretty_version' => 'v2.2.2',
             'version' => '2.2.2.0',
+            'reference' => 'eccf915f45f911bfb189d1d1638d940ec6ee6e33',
             'type' => 'library',
             'install_path' => __DIR__ . '/../paragonie/constant_time_encoding',
             'aliases' => array(),
-            'reference' => 'eccf915f45f911bfb189d1d1638d940ec6ee6e33',
             'dev_requirement' => false,
         ),
         'paragonie/random_compat' => array(
             'pretty_version' => 'v9.99.100',
             'version' => '9.99.100.0',
+            'reference' => '996434e5492cb4c3edcb9168db6fbb1359ef965a',
             'type' => 'library',
             'install_path' => __DIR__ . '/../paragonie/random_compat',
             'aliases' => array(),
-            'reference' => '996434e5492cb4c3edcb9168db6fbb1359ef965a',
             'dev_requirement' => false,
         ),
         'paragonie/sodium_compat' => array(
             'pretty_version' => 'v1.6.4',
             'version' => '1.6.4.0',
+            'reference' => '3f2fd07977541b4d630ea0365ad0eceddee5179c',
             'type' => 'library',
             'install_path' => __DIR__ . '/../paragonie/sodium_compat',
             'aliases' => array(),
-            'reference' => '3f2fd07977541b4d630ea0365ad0eceddee5179c',
             'dev_requirement' => false,
         ),
         'peppeocchi/php-cron-scheduler' => array(
             'pretty_version' => 'v4.0',
             'version' => '4.0.0.0',
+            'reference' => '0acfa032e60f0ea22a27b96a6b15a673a31d3448',
             'type' => 'library',
             'install_path' => __DIR__ . '/../peppeocchi/php-cron-scheduler',
             'aliases' => array(),
-            'reference' => '0acfa032e60f0ea22a27b96a6b15a673a31d3448',
             'dev_requirement' => false,
         ),
         'php-http/httplug' => array(
             'pretty_version' => '2.2.0',
             'version' => '2.2.0.0',
+            'reference' => '191a0a1b41ed026b717421931f8d3bd2514ffbf9',
             'type' => 'library',
             'install_path' => __DIR__ . '/../php-http/httplug',
             'aliases' => array(),
-            'reference' => '191a0a1b41ed026b717421931f8d3bd2514ffbf9',
             'dev_requirement' => false,
         ),
         'php-http/promise' => array(
             'pretty_version' => '1.1.0',
             'version' => '1.1.0.0',
+            'reference' => '4c4c1f9b7289a2ec57cde7f1e9762a5789506f88',
             'type' => 'library',
             'install_path' => __DIR__ . '/../php-http/promise',
             'aliases' => array(),
-            'reference' => '4c4c1f9b7289a2ec57cde7f1e9762a5789506f88',
             'dev_requirement' => false,
         ),
         'phpmailer/phpmailer' => array(
             'pretty_version' => 'v6.5.0',
             'version' => '6.5.0.0',
+            'reference' => 'a5b5c43e50b7fba655f793ad27303cd74c57363c',
             'type' => 'library',
             'install_path' => __DIR__ . '/../phpmailer/phpmailer',
             'aliases' => array(),
-            'reference' => 'a5b5c43e50b7fba655f793ad27303cd74c57363c',
             'dev_requirement' => false,
         ),
         'pragmarx/google2fa' => array(
             'pretty_version' => 'v3.0.3',
             'version' => '3.0.3.0',
+            'reference' => '6949226739e4424f40031e6f1c96b1fd64047335',
             'type' => 'library',
             'install_path' => __DIR__ . '/../pragmarx/google2fa',
             'aliases' => array(),
-            'reference' => '6949226739e4424f40031e6f1c96b1fd64047335',
             'dev_requirement' => false,
         ),
         'psr/container' => array(
             'pretty_version' => '1.1.1',
             'version' => '1.1.1.0',
+            'reference' => '8622567409010282b7aeebe4bb841fe98b58dcaf',
             'type' => 'library',
             'install_path' => __DIR__ . '/../psr/container',
             'aliases' => array(),
-            'reference' => '8622567409010282b7aeebe4bb841fe98b58dcaf',
             'dev_requirement' => false,
         ),
         'psr/http-client' => array(
             'pretty_version' => '1.0.1',
             'version' => '1.0.1.0',
+            'reference' => '2dfb5f6c5eff0e91e20e913f8c5452ed95b86621',
             'type' => 'library',
             'install_path' => __DIR__ . '/../psr/http-client',
             'aliases' => array(),
-            'reference' => '2dfb5f6c5eff0e91e20e913f8c5452ed95b86621',
             'dev_requirement' => false,
         ),
         'psr/http-client-implementation' => array(
@@ -373,10 +373,10 @@
         'psr/http-factory' => array(
             'pretty_version' => '1.0.1',
             'version' => '1.0.1.0',
+            'reference' => '12ac7fcd07e5b077433f5f2bee95b3a771bf61be',
             'type' => 'library',
             'install_path' => __DIR__ . '/../psr/http-factory',
             'aliases' => array(),
-            'reference' => '12ac7fcd07e5b077433f5f2bee95b3a771bf61be',
             'dev_requirement' => false,
         ),
         'psr/http-factory-implementation' => array(
@@ -388,10 +388,10 @@
         'psr/http-message' => array(
             'pretty_version' => '1.0.1',
             'version' => '1.0.1.0',
+            'reference' => 'f6561bf28d520154e4b0ec72be95418abe6d9363',
             'type' => 'library',
             'install_path' => __DIR__ . '/../psr/http-message',
             'aliases' => array(),
-            'reference' => 'f6561bf28d520154e4b0ec72be95418abe6d9363',
             'dev_requirement' => false,
         ),
         'psr/http-message-implementation' => array(
@@ -403,28 +403,28 @@
         'psr/http-server-handler' => array(
             'pretty_version' => '1.0.1',
             'version' => '1.0.1.0',
+            'reference' => 'aff2f80e33b7f026ec96bb42f63242dc50ffcae7',
             'type' => 'library',
             'install_path' => __DIR__ . '/../psr/http-server-handler',
             'aliases' => array(),
-            'reference' => 'aff2f80e33b7f026ec96bb42f63242dc50ffcae7',
             'dev_requirement' => false,
         ),
         'psr/http-server-middleware' => array(
             'pretty_version' => '1.0.1',
             'version' => '1.0.1.0',
+            'reference' => '2296f45510945530b9dceb8bcedb5cb84d40c5f5',
             'type' => 'library',
             'install_path' => __DIR__ . '/../psr/http-server-middleware',
             'aliases' => array(),
-            'reference' => '2296f45510945530b9dceb8bcedb5cb84d40c5f5',
             'dev_requirement' => false,
         ),
         'psr/log' => array(
             'pretty_version' => '1.1.4',
             'version' => '1.1.4.0',
+            'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11',
             'type' => 'library',
             'install_path' => __DIR__ . '/../psr/log',
             'aliases' => array(),
-            'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11',
             'dev_requirement' => false,
         ),
         'psr/log-implementation' => array(
@@ -436,46 +436,46 @@
         'psr/simple-cache' => array(
             'pretty_version' => '1.0.1',
             'version' => '1.0.1.0',
+            'reference' => '408d5eafb83c57f6365a3ca330ff23aa4a5fa39b',
             'type' => 'library',
             'install_path' => __DIR__ . '/../psr/simple-cache',
             'aliases' => array(),
-            'reference' => '408d5eafb83c57f6365a3ca330ff23aa4a5fa39b',
             'dev_requirement' => false,
         ),
         'pusher/pusher-php-server' => array(
             'pretty_version' => 'v4.1.5',
             'version' => '4.1.5.0',
+            'reference' => '251f22602320c1b1aff84798fe74f3f7ee0504a9',
             'type' => 'library',
             'install_path' => __DIR__ . '/../pusher/pusher-php-server',
             'aliases' => array(),
-            'reference' => '251f22602320c1b1aff84798fe74f3f7ee0504a9',
             'dev_requirement' => false,
         ),
         'ralouphie/getallheaders' => array(
             'pretty_version' => '3.0.3',
             'version' => '3.0.3.0',
+            'reference' => '120b605dfeb996808c31b6477290a714d356e822',
             'type' => 'library',
             'install_path' => __DIR__ . '/../ralouphie/getallheaders',
             'aliases' => array(),
-            'reference' => '120b605dfeb996808c31b6477290a714d356e822',
             'dev_requirement' => false,
         ),
         'ramsey/collection' => array(
             'pretty_version' => '1.2.2',
             'version' => '1.2.2.0',
+            'reference' => 'cccc74ee5e328031b15640b51056ee8d3bb66c0a',
             'type' => 'library',
             'install_path' => __DIR__ . '/../ramsey/collection',
             'aliases' => array(),
-            'reference' => 'cccc74ee5e328031b15640b51056ee8d3bb66c0a',
             'dev_requirement' => false,
         ),
         'ramsey/uuid' => array(
             'pretty_version' => '4.2.3',
             'version' => '4.2.3.0',
+            'reference' => 'fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df',
             'type' => 'library',
             'install_path' => __DIR__ . '/../ramsey/uuid',
             'aliases' => array(),
-            'reference' => 'fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df',
             'dev_requirement' => false,
         ),
         'rhumsaa/uuid' => array(
@@ -487,172 +487,172 @@
         'rmccue/requests' => array(
             'pretty_version' => 'v1.8.0',
             'version' => '1.8.0.0',
+            'reference' => 'afbe4790e4def03581c4a0963a1e8aa01f6030f1',
             'type' => 'library',
             'install_path' => __DIR__ . '/../rmccue/requests',
             'aliases' => array(),
-            'reference' => 'afbe4790e4def03581c4a0963a1e8aa01f6030f1',
             'dev_requirement' => false,
         ),
         'simshaun/recurr' => array(
             'pretty_version' => 'v5.0.0',
             'version' => '5.0.0.0',
+            'reference' => 'b5aa5b07a595023b67a558b810390dfa7160e3f5',
             'type' => 'library',
             'install_path' => __DIR__ . '/../simshaun/recurr',
             'aliases' => array(),
-            'reference' => 'b5aa5b07a595023b67a558b810390dfa7160e3f5',
             'dev_requirement' => false,
         ),
         'slim/psr7' => array(
             'pretty_version' => '1.3.0',
             'version' => '1.3.0.0',
+            'reference' => '235d2e5a5ee1ad4b97b96870f37f3091b22fffd7',
             'type' => 'library',
             'install_path' => __DIR__ . '/../slim/psr7',
             'aliases' => array(),
-            'reference' => '235d2e5a5ee1ad4b97b96870f37f3091b22fffd7',
             'dev_requirement' => false,
         ),
         'slim/slim' => array(
             'pretty_version' => '4.7.1',
             'version' => '4.7.1.0',
+            'reference' => '0905e0775f8c1cfb3bbcfabeb6588dcfd8b82d3f',
             'type' => 'library',
             'install_path' => __DIR__ . '/../slim/slim',
             'aliases' => array(),
-            'reference' => '0905e0775f8c1cfb3bbcfabeb6588dcfd8b82d3f',
             'dev_requirement' => false,
         ),
         'stripe/stripe-php' => array(
             'pretty_version' => 'v7.116.0',
             'version' => '7.116.0.0',
+            'reference' => '7a39f594f213ed3f443a95adf769d1ecbc8393e7',
             'type' => 'library',
             'install_path' => __DIR__ . '/../stripe/stripe-php',
             'aliases' => array(),
-            'reference' => '7a39f594f213ed3f443a95adf769d1ecbc8393e7',
             'dev_requirement' => false,
         ),
         'symfony/deprecation-contracts' => array(
             'pretty_version' => 'v2.1.3',
             'version' => '2.1.3.0',
+            'reference' => '5e20b83385a77593259c9f8beb2c43cd03b2ac14',
             'type' => 'library',
             'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
             'aliases' => array(),
-            'reference' => '5e20b83385a77593259c9f8beb2c43cd03b2ac14',
             'dev_requirement' => false,
         ),
         'symfony/finder' => array(
             'pretty_version' => 'v5.1.3',
             'version' => '5.1.3.0',
+            'reference' => '4298870062bfc667cb78d2b379be4bf5dec5f187',
             'type' => 'library',
             'install_path' => __DIR__ . '/../symfony/finder',
             'aliases' => array(),
-            'reference' => '4298870062bfc667cb78d2b379be4bf5dec5f187',
             'dev_requirement' => false,
         ),
         'symfony/polyfill-ctype' => array(
             'pretty_version' => 'v1.22.1',
             'version' => '1.22.1.0',
+            'reference' => 'c6c942b1ac76c82448322025e084cadc56048b4e',
             'type' => 'library',
             'install_path' => __DIR__ . '/../symfony/polyfill-ctype',
             'aliases' => array(),
-            'reference' => 'c6c942b1ac76c82448322025e084cadc56048b4e',
             'dev_requirement' => false,
         ),
         'symfony/polyfill-mbstring' => array(
             'pretty_version' => 'v1.22.1',
             'version' => '1.22.1.0',
+            'reference' => '5232de97ee3b75b0360528dae24e73db49566ab1',
             'type' => 'library',
             'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
             'aliases' => array(),
-            'reference' => '5232de97ee3b75b0360528dae24e73db49566ab1',
             'dev_requirement' => false,
         ),
         'symfony/polyfill-php56' => array(
             'pretty_version' => 'v1.9.0',
             'version' => '1.9.0.0',
+            'reference' => '7b4fc009172cc0196535b0328bd1226284a28000',
             'type' => 'library',
             'install_path' => __DIR__ . '/../symfony/polyfill-php56',
             'aliases' => array(),
-            'reference' => '7b4fc009172cc0196535b0328bd1226284a28000',
             'dev_requirement' => false,
         ),
         'symfony/polyfill-php72' => array(
             'pretty_version' => 'v1.22.1',
             'version' => '1.22.1.0',
+            'reference' => 'cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9',
             'type' => 'library',
             'install_path' => __DIR__ . '/../symfony/polyfill-php72',
             'aliases' => array(),
-            'reference' => 'cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9',
             'dev_requirement' => false,
         ),
         'symfony/polyfill-php80' => array(
             'pretty_version' => 'v1.22.1',
             'version' => '1.22.1.0',
+            'reference' => 'dc3063ba22c2a1fd2f45ed856374d79114998f91',
             'type' => 'library',
             'install_path' => __DIR__ . '/../symfony/polyfill-php80',
             'aliases' => array(),
-            'reference' => 'dc3063ba22c2a1fd2f45ed856374d79114998f91',
             'dev_requirement' => false,
         ),
         'symfony/polyfill-php81' => array(
             'pretty_version' => 'v1.25.0',
             'version' => '1.25.0.0',
+            'reference' => '5de4ba2d41b15f9bd0e19b2ab9674135813ec98f',
             'type' => 'library',
             'install_path' => __DIR__ . '/../symfony/polyfill-php81',
             'aliases' => array(),
-            'reference' => '5de4ba2d41b15f9bd0e19b2ab9674135813ec98f',
             'dev_requirement' => false,
         ),
         'symfony/polyfill-util' => array(
             'pretty_version' => 'v1.9.0',
             'version' => '1.9.0.0',
+            'reference' => '8e15d04ba3440984d23e7964b2ee1d25c8de1581',
             'type' => 'library',
             'install_path' => __DIR__ . '/../symfony/polyfill-util',
             'aliases' => array(),
-            'reference' => '8e15d04ba3440984d23e7964b2ee1d25c8de1581',
             'dev_requirement' => false,
         ),
         'symfony/var-dumper' => array(
             'pretty_version' => 'v4.2.3',
             'version' => '4.2.3.0',
+            'reference' => '223bda89f9be41cf7033eeaf11bc61a280489c17',
             'type' => 'library',
             'install_path' => __DIR__ . '/../symfony/var-dumper',
             'aliases' => array(),
-            'reference' => '223bda89f9be41cf7033eeaf11bc61a280489c17',
             'dev_requirement' => false,
         ),
         'symfony/yaml' => array(
             'pretty_version' => 'v5.1.3',
             'version' => '5.1.3.0',
+            'reference' => 'ea342353a3ef4f453809acc4ebc55382231d4d23',
             'type' => 'library',
             'install_path' => __DIR__ . '/../symfony/yaml',
             'aliases' => array(),
-            'reference' => 'ea342353a3ef4f453809acc4ebc55382231d4d23',
             'dev_requirement' => false,
         ),
         'tightenco/collect' => array(
             'pretty_version' => 'v5.7.27',
             'version' => '5.7.27.0',
+            'reference' => 'c1a36a2a8a0aa731c1acdcd83f57724ffe630d00',
             'type' => 'library',
             'install_path' => __DIR__ . '/../tightenco/collect',
             'aliases' => array(),
-            'reference' => 'c1a36a2a8a0aa731c1acdcd83f57724ffe630d00',
             'dev_requirement' => false,
         ),
         'webmozart/assert' => array(
             'pretty_version' => '1.10.0',
             'version' => '1.10.0.0',
+            'reference' => '6964c76c7804814a842473e0c8fd15bab0f18e25',
             'type' => 'library',
             'install_path' => __DIR__ . '/../webmozart/assert',
             'aliases' => array(),
-            'reference' => '6964c76c7804814a842473e0c8fd15bab0f18e25',
             'dev_requirement' => false,
         ),
         'zircote/swagger-php' => array(
             'pretty_version' => '3.0.4',
             'version' => '3.0.4.0',
+            'reference' => 'fa47d62c22c95272625624fbf8109fa46ffac43b',
             'type' => 'library',
             'install_path' => __DIR__ . '/../zircote/swagger-php',
             'aliases' => array(),
-            'reference' => 'fa47d62c22c95272625624fbf8109fa46ffac43b',
             'dev_requirement' => false,
         ),
     ),

+ 2 - 3
api/vendor/composer/platform_check.php

@@ -19,8 +19,7 @@ if ($issues) {
             echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
         }
     }
-    trigger_error(
-        'Composer detected issues in your platform: ' . implode(' ', $issues),
-        E_USER_ERROR
+    throw new \RuntimeException(
+        'Composer detected issues in your platform: ' . implode(' ', $issues)
     );
 }

+ 20 - 233
api/vendor/league/oauth2-client/README.md

@@ -1,268 +1,55 @@
 # OAuth 2.0 Client
 
-This package makes it simple to integrate your application with [OAuth 2.0](http://oauth.net/2/) service providers.
+This package provides a base for integrating with [OAuth 2.0](http://oauth.net/2/) service providers.
 
 [![Gitter Chat](https://img.shields.io/badge/gitter-join_chat-brightgreen.svg?style=flat-square)](https://gitter.im/thephpleague/oauth2-client)
 [![Source Code](https://img.shields.io/badge/source-thephpleague/oauth2--client-blue.svg?style=flat-square)](https://github.com/thephpleague/oauth2-client)
 [![Latest Version](https://img.shields.io/github/release/thephpleague/oauth2-client.svg?style=flat-square)](https://github.com/thephpleague/oauth2-client/releases)
 [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](https://github.com/thephpleague/oauth2-client/blob/master/LICENSE)
-[![Build Status](https://img.shields.io/github/workflow/status/thephpleague/oauth2-client/CI?label=CI&logo=github&style=flat-square)](https://github.com/thephpleague/oauth2-client/actions?query=workflow%3ACI)
+[![Build Status](https://img.shields.io/github/actions/workflow/status/thephpleague/oauth2-client/continuous-integration.yml?label=CI&logo=github&style=flat-square)](https://github.com/thephpleague/oauth2-client/actions?query=workflow%3ACI)
 [![Codecov Code Coverage](https://img.shields.io/codecov/c/gh/thephpleague/oauth2-client?label=codecov&logo=codecov&style=flat-square)](https://codecov.io/gh/thephpleague/oauth2-client)
 [![Total Downloads](https://img.shields.io/packagist/dt/league/oauth2-client.svg?style=flat-square)](https://packagist.org/packages/league/oauth2-client)
 
 ---
 
-We are all used to seeing those "Connect with Facebook/Google/etc." buttons around the internet, and social network integration is an important feature of most web applications these days. Many of these sites use an authentication and authorization standard called OAuth 2.0 ([RFC 6749](http://tools.ietf.org/html/rfc6749)).
+The OAuth 2.0 login flow, seen commonly around the web in the form of "Connect with Facebook/Google/etc." buttons, is a common integration added to web applications, but it can be tricky and tedious to do right. To help, we've created the `league/oauth2-client` package, which provides a base for integrating with various OAuth 2.0 providers, without overburdening your application with the concerns of [RFC 6749](http://tools.ietf.org/html/rfc6749).
 
-This OAuth 2.0 client library will work with any OAuth provider that conforms to the OAuth 2.0 standard. Out-of-the-box, we provide a `GenericProvider` that may be used to connect to any service provider that uses [Bearer tokens](http://tools.ietf.org/html/rfc6750) (see example below).
+This OAuth 2.0 client library will work with any OAuth 2.0 provider that conforms to the OAuth 2.0 Authorization Framework. Out-of-the-box, we provide a `GenericProvider` class to connect to any service provider that uses [Bearer tokens](http://tools.ietf.org/html/rfc6750). See our [basic usage guide](https://oauth2-client.thephpleague.com/usage/) for examples using `GenericProvider`.
 
-Many service providers provide additional functionality above and beyond the OAuth 2.0 standard. For this reason, this library may be easily extended and wrapped to support this additional behavior. We provide links to [all known provider clients extending this library](docs/providers/thirdparty.md) (i.e. Facebook, GitHub, Google, Instagram, LinkedIn, etc.). If your provider isn't in the list, feel free to add it.
+Many service providers provide additional functionality above and beyond the OAuth 2.0 specification. For this reason, you may extend and wrap this library to support additional behavior. There are already many [official](https://oauth2-client.thephpleague.com/providers/league/) and [third-party](https://oauth2-client.thephpleague.com/providers/thirdparty/) provider clients available (e.g., Facebook, GitHub, Google, Instagram, LinkedIn, etc.). If your provider isn't in the list, feel free to add it.
 
-This package is compliant with [PSR-1][], [PSR-2][], [PSR-4][], and [PSR-7][]. If you notice compliance oversights, please send a patch via pull request. If you're interesting in contributing to this library, please take a look at our [contributing guidelines](CONTRIBUTING.md).
+This package is compliant with [PSR-1][], [PSR-2][], [PSR-4][], and [PSR-7][]. If you notice compliance oversights, please send a patch via pull request. If you're interested in contributing to this library, please take a look at our [contributing guidelines](https://github.com/thephpleague/oauth2-client/blob/master/CONTRIBUTING.md).
 
 ## Requirements
 
-The following versions of PHP are supported.
+We support the following versions of PHP:
 
-* PHP 5.6
-* PHP 7.0
-* PHP 7.1
-* PHP 7.2
-* PHP 7.3
-* PHP 7.4
+* PHP 8.1
 * PHP 8.0
+* PHP 7.4
+* PHP 7.3
+* PHP 7.2
+* PHP 7.1
+* PHP 7.0
+* PHP 5.6
 
-## Providers
+## Provider Clients
 
-A list of official PHP League providers, as well as third-party providers, may be found in the [providers list README](docs/providers/thirdparty.md).
+We provide a list of [official PHP League provider clients](https://oauth2-client.thephpleague.com/providers/league/), as well as [third-party provider clients](https://oauth2-client.thephpleague.com/providers/thirdparty/).
 
-To build your own provider, please refer to the [provider guide README](README.PROVIDER-GUIDE.md).
+To build your own provider client, please refer to "[Implementing a Provider Client](https://oauth2-client.thephpleague.com/providers/implementing/)."
 
 ## Usage
 
-**In most cases, you'll want to use a specific provider client library rather than this base library.**
-
-Take a look at [providers list README](docs/providers/thirdparty.md) to see a list of provider client libraries.
-
-If using Composer to require a specific provider client library, you **do not need to also require this library**. Composer will handle the dependencies for you.
-
-### Authorization Code Grant
-
-The following example uses the out-of-the-box `GenericProvider` provided by this library. If you're looking for a specific provider (i.e. Facebook, Google, GitHub, etc.), take a look at our [list of provider client libraries](docs/providers/thirdparty.md). **HINT: You're probably looking for a specific provider.**
-
-The authorization code grant type is the most common grant type used when authenticating users with a third-party service. This grant type utilizes a client (this library), a server (the service provider), and a resource owner (the user with credentials to a protected—or owned—resource) to request access to resources owned by the user. This is often referred to as _3-legged OAuth_, since there are three parties involved.
-
-The following example illustrates this using [Brent Shaffer's](https://github.com/bshaffer) demo OAuth 2.0 application named **Lock'd In**. When running this code, you will be redirected to Lock'd In, where you'll be prompted to authorize the client to make requests to a resource on your behalf.
-
-Now, you don't really have an account on Lock'd In, but for the sake of this example, imagine that you are already logged in on Lock'd In when you are redirected there.
-
-```php
-$provider = new \League\OAuth2\Client\Provider\GenericProvider([
-    'clientId'                => 'demoapp',    // The client ID assigned to you by the provider
-    'clientSecret'            => 'demopass',   // The client password assigned to you by the provider
-    'redirectUri'             => 'http://example.com/your-redirect-url/',
-    'urlAuthorize'            => 'http://brentertainment.com/oauth2/lockdin/authorize',
-    'urlAccessToken'          => 'http://brentertainment.com/oauth2/lockdin/token',
-    'urlResourceOwnerDetails' => 'http://brentertainment.com/oauth2/lockdin/resource'
-]);
-
-// If we don't have an authorization code then get one
-if (!isset($_GET['code'])) {
-
-    // Fetch the authorization URL from the provider; this returns the
-    // urlAuthorize option and generates and applies any necessary parameters
-    // (e.g. state).
-    $authorizationUrl = $provider->getAuthorizationUrl();
-
-    // Get the state generated for you and store it to the session.
-    $_SESSION['oauth2state'] = $provider->getState();
-
-    // Redirect the user to the authorization URL.
-    header('Location: ' . $authorizationUrl);
-    exit;
-
-// Check given state against previously stored one to mitigate CSRF attack
-} elseif (empty($_GET['state']) || (isset($_SESSION['oauth2state']) && $_GET['state'] !== $_SESSION['oauth2state'])) {
-
-    if (isset($_SESSION['oauth2state'])) {
-        unset($_SESSION['oauth2state']);
-    }
-    
-    exit('Invalid state');
-
-} else {
-
-    try {
-
-        // Try to get an access token using the authorization code grant.
-        $accessToken = $provider->getAccessToken('authorization_code', [
-            'code' => $_GET['code']
-        ]);
-
-        // We have an access token, which we may use in authenticated
-        // requests against the service provider's API.
-        echo 'Access Token: ' . $accessToken->getToken() . "<br>";
-        echo 'Refresh Token: ' . $accessToken->getRefreshToken() . "<br>";
-        echo 'Expired in: ' . $accessToken->getExpires() . "<br>";
-        echo 'Already expired? ' . ($accessToken->hasExpired() ? 'expired' : 'not expired') . "<br>";
-
-        // Using the access token, we may look up details about the
-        // resource owner.
-        $resourceOwner = $provider->getResourceOwner($accessToken);
-
-        var_export($resourceOwner->toArray());
-
-        // The provider provides a way to get an authenticated API request for
-        // the service, using the access token; it returns an object conforming
-        // to Psr\Http\Message\RequestInterface.
-        $request = $provider->getAuthenticatedRequest(
-            'GET',
-            'http://brentertainment.com/oauth2/lockdin/resource',
-            $accessToken
-        );
-
-    } catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {
-
-        // Failed to get the access token or user details.
-        exit($e->getMessage());
-
-    }
-
-}
-```
-
-### Refreshing a Token
-
-Once your application is authorized, you can refresh an expired token using a refresh token rather than going through the entire process of obtaining a brand new token. To do so, simply reuse this refresh token from your data store to request a refresh.
-
-_This example uses [Brent Shaffer's](https://github.com/bshaffer) demo OAuth 2.0 application named **Lock'd In**. See authorization code example above, for more details._
-
-```php
-$provider = new \League\OAuth2\Client\Provider\GenericProvider([
-    'clientId'                => 'demoapp',    // The client ID assigned to you by the provider
-    'clientSecret'            => 'demopass',   // The client password assigned to you by the provider
-    'redirectUri'             => 'http://example.com/your-redirect-url/',
-    'urlAuthorize'            => 'http://brentertainment.com/oauth2/lockdin/authorize',
-    'urlAccessToken'          => 'http://brentertainment.com/oauth2/lockdin/token',
-    'urlResourceOwnerDetails' => 'http://brentertainment.com/oauth2/lockdin/resource'
-]);
-
-$existingAccessToken = getAccessTokenFromYourDataStore();
-
-if ($existingAccessToken->hasExpired()) {
-    $newAccessToken = $provider->getAccessToken('refresh_token', [
-        'refresh_token' => $existingAccessToken->getRefreshToken()
-    ]);
-
-    // Purge old access token and store new access token to your data store.
-}
-```
-
-### Resource Owner Password Credentials Grant
-
-Some service providers allow you to skip the authorization code step to exchange a user's credentials (username and password) for an access token. This is referred to as the "resource owner password credentials" grant type.
-
-According to [section 1.3.3](http://tools.ietf.org/html/rfc6749#section-1.3.3) of the OAuth 2.0 standard (emphasis added):
-
-> The credentials **should only be used when there is a high degree of trust**
-> between the resource owner and the client (e.g., the client is part of the
-> device operating system or a highly privileged application), and when other
-> authorization grant types are not available (such as an authorization code).
-
-**We do not advise using this grant type if the service provider supports the authorization code grant type (see above), as this reinforces the [password anti-pattern](https://agentile.com/the-password-anti-pattern) by allowing users to think it's okay to trust third-party applications with their usernames and passwords.**
-
-That said, there are use-cases where the resource owner password credentials grant is acceptable and useful. Here's an example using it with [Brent Shaffer's](https://github.com/bshaffer) demo OAuth 2.0 application named **Lock'd In**. See authorization code example above, for more details about the Lock'd In demo application.
-
-``` php
-$provider = new \League\OAuth2\Client\Provider\GenericProvider([
-    'clientId'                => 'demoapp',    // The client ID assigned to you by the provider
-    'clientSecret'            => 'demopass',   // The client password assigned to you by the provider
-    'redirectUri'             => 'http://example.com/your-redirect-url/',
-    'urlAuthorize'            => 'http://brentertainment.com/oauth2/lockdin/authorize',
-    'urlAccessToken'          => 'http://brentertainment.com/oauth2/lockdin/token',
-    'urlResourceOwnerDetails' => 'http://brentertainment.com/oauth2/lockdin/resource'
-]);
-
-try {
-
-    // Try to get an access token using the resource owner password credentials grant.
-    $accessToken = $provider->getAccessToken('password', [
-        'username' => 'demouser',
-        'password' => 'testpass'
-    ]);
-
-} catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {
-
-    // Failed to get the access token
-    exit($e->getMessage());
-
-}
-```
-
-### Client Credentials Grant
-
-When your application is acting on its own behalf to access resources it controls/owns in a service provider, it may use the client credentials grant type. This is best used when the credentials for your application are stored privately and never exposed (e.g. through the web browser, etc.) to end-users. This grant type functions similarly to the resource owner password credentials grant type, but it does not request a user's username or password. It uses only the client ID and secret issued to your client by the service provider.
-
-Unlike earlier examples, the following does not work against a functioning demo service provider. It is provided for the sake of example only.
-
-``` php
-// Note: the GenericProvider requires the `urlAuthorize` option, even though
-// it's not used in the OAuth 2.0 client credentials grant type.
-
-$provider = new \League\OAuth2\Client\Provider\GenericProvider([
-    'clientId'                => 'XXXXXX',    // The client ID assigned to you by the provider
-    'clientSecret'            => 'XXXXXX',    // The client password assigned to you by the provider
-    'redirectUri'             => 'http://my.example.com/your-redirect-url/',
-    'urlAuthorize'            => 'http://service.example.com/authorize',
-    'urlAccessToken'          => 'http://service.example.com/token',
-    'urlResourceOwnerDetails' => 'http://service.example.com/resource'
-]);
-
-try {
-
-    // Try to get an access token using the client credentials grant.
-    $accessToken = $provider->getAccessToken('client_credentials');
-
-} catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {
-
-    // Failed to get the access token
-    exit($e->getMessage());
-
-}
-```
-
-### Using a proxy
-
-It is possible to use a proxy to debug HTTP calls made to a provider. All you need to do is set the `proxy` and `verify` options when creating your Provider instance. Make sure you enable SSL proxying in your proxy.
-
-``` php
-$provider = new \League\OAuth2\Client\Provider\GenericProvider([
-    'clientId'                => 'XXXXXX',    // The client ID assigned to you by the provider
-    'clientSecret'            => 'XXXXXX',    // The client password assigned to you by the provider
-    'redirectUri'             => 'http://my.example.com/your-redirect-url/',
-    'urlAuthorize'            => 'http://service.example.com/authorize',
-    'urlAccessToken'          => 'http://service.example.com/token',
-    'urlResourceOwnerDetails' => 'http://service.example.com/resource',
-    'proxy'                   => '192.168.0.1:8888',
-    'verify'                  => false
-]);
-```
-
-## Install
-
-Via Composer
-
-``` bash
-$ composer require league/oauth2-client
-```
+For usage and code examples, check out our [basic usage guide](https://oauth2-client.thephpleague.com/usage/).
 
 ## Contributing
 
-Please see [CONTRIBUTING](https://github.com/thephpleague/oauth2-client/blob/master/CONTRIBUTING.md) for details.
+Please see [our contributing guidelines](https://github.com/thephpleague/oauth2-client/blob/master/CONTRIBUTING.md) for details.
 
 ## License
 
-The MIT License (MIT). Please see [License File](https://github.com/thephpleague/oauth2-client/blob/master/LICENSE) for more information.
+The MIT License (MIT). Please see [LICENSE](https://github.com/thephpleague/oauth2-client/blob/master/LICENSE) for more information.
 
 
 [PSR-1]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md

+ 3 - 3
api/vendor/league/oauth2-client/composer.json

@@ -11,9 +11,9 @@
         "paragonie/random_compat": "^1 || ^2 || ^9.99"
     },
     "require-dev": {
-        "mockery/mockery": "^1.3",
-        "php-parallel-lint/php-parallel-lint": "^1.2",
-        "phpunit/phpunit": "^5.7 || ^6.0 || ^9.3",
+        "mockery/mockery": "^1.3.5",
+        "php-parallel-lint/php-parallel-lint": "^1.3.1",
+        "phpunit/phpunit": "^5.7 || ^6.0 || ^9.5",
         "squizlabs/php_codesniffer": "^2.3 || ^3.0"
     },
     "keywords": [

+ 102 - 4
api/vendor/league/oauth2-client/src/Provider/AbstractProvider.php

@@ -17,6 +17,7 @@ namespace League\OAuth2\Client\Provider;
 use GuzzleHttp\Client as HttpClient;
 use GuzzleHttp\ClientInterface as HttpClientInterface;
 use GuzzleHttp\Exception\BadResponseException;
+use InvalidArgumentException;
 use League\OAuth2\Client\Grant\AbstractGrant;
 use League\OAuth2\Client\Grant\GrantFactory;
 use League\OAuth2\Client\OptionProvider\OptionProviderInterface;
@@ -44,7 +45,7 @@ abstract class AbstractProvider
     use QueryBuilderTrait;
 
     /**
-     * @var string Key used in a token response to identify the resource owner.
+     * @var string|null Key used in a token response to identify the resource owner.
      */
     const ACCESS_TOKEN_RESOURCE_OWNER_ID = null;
 
@@ -58,6 +59,19 @@ abstract class AbstractProvider
      */
     const METHOD_POST = 'POST';
 
+    /**
+     * @var string PKCE method used to fetch authorization token.
+     * The PKCE code challenge will be hashed with sha256 (recommended).
+     */
+    const PKCE_METHOD_S256 = 'S256';
+
+    /**
+     * @var string PKCE method used to fetch authorization token.
+     * The PKCE code challenge will be sent as plain text, this is NOT recommended.
+     * Only use `plain` if no other option is possible.
+     */
+    const PKCE_METHOD_PLAIN = 'plain';
+
     /**
      * @var string
      */
@@ -78,6 +92,11 @@ abstract class AbstractProvider
      */
     protected $state;
 
+    /**
+     * @var string|null
+     */
+    protected $pkceCode = null;
+
     /**
      * @var GrantFactory
      */
@@ -264,6 +283,32 @@ abstract class AbstractProvider
         return $this->state;
     }
 
+    /**
+     * Set the value of the pkceCode parameter.
+     *
+     * When using PKCE this should be set before requesting an access token.
+     *
+     * @param string $pkceCode
+     * @return self
+     */
+    public function setPkceCode($pkceCode)
+    {
+        $this->pkceCode = $pkceCode;
+        return $this;
+    }
+
+    /**
+     * Returns the current value of the pkceCode parameter.
+     *
+     * This can be accessed by the redirect handler during authorization.
+     *
+     * @return string|null
+     */
+    public function getPkceCode()
+    {
+        return $this->pkceCode;
+    }
+
     /**
      * Returns the base URL for authorizing a client.
      *
@@ -305,6 +350,27 @@ abstract class AbstractProvider
         return bin2hex(random_bytes($length / 2));
     }
 
+    /**
+     * Returns a new random string to use as PKCE code_verifier and
+     * hashed as code_challenge parameters in an authorization flow.
+     * Must be between 43 and 128 characters long.
+     *
+     * @param  int $length Length of the random string to be generated.
+     * @return string
+     */
+    protected function getRandomPkceCode($length = 64)
+    {
+        return substr(
+            strtr(
+                base64_encode(random_bytes($length)),
+                '+/',
+                '-_'
+            ),
+            0,
+            $length
+        );
+    }
+
     /**
      * Returns the default scopes used by this provider.
      *
@@ -326,6 +392,14 @@ abstract class AbstractProvider
         return ',';
     }
 
+    /**
+     * @return string|null
+     */
+    protected function getPkceMethod()
+    {
+        return null;
+    }
+
     /**
      * Returns authorization parameters based on provided options.
      *
@@ -355,6 +429,26 @@ abstract class AbstractProvider
         // Store the state as it may need to be accessed later on.
         $this->state = $options['state'];
 
+        $pkceMethod = $this->getPkceMethod();
+        if (!empty($pkceMethod)) {
+            $this->pkceCode = $this->getRandomPkceCode();
+            if ($pkceMethod === static::PKCE_METHOD_S256) {
+                $options['code_challenge'] = trim(
+                    strtr(
+                        base64_encode(hash('sha256', $this->pkceCode, true)),
+                        '+/',
+                        '-_'
+                    ),
+                    '='
+                );
+            } elseif ($pkceMethod === static::PKCE_METHOD_PLAIN) {
+                $options['code_challenge'] = $this->pkceCode;
+            } else {
+                throw new InvalidArgumentException('Unknown PKCE method "' . $pkceMethod . '".');
+            }
+            $options['code_challenge_method'] = $pkceMethod;
+        }
+
         // Business code layer might set a different redirect_uri parameter
         // depending on the context, leave it as-is
         if (!isset($options['redirect_uri'])) {
@@ -517,8 +611,8 @@ abstract class AbstractProvider
     /**
      * Requests an access token using a specified grant and option set.
      *
-     * @param  mixed $grant
-     * @param  array $options
+     * @param  mixed                $grant
+     * @param  array<string, mixed> $options
      * @throws IdentityProviderException
      * @return AccessTokenInterface
      */
@@ -532,6 +626,10 @@ abstract class AbstractProvider
             'redirect_uri'  => $this->redirectUri,
         ];
 
+        if (!empty($this->pkceCode)) {
+            $params['code_verifier'] = $this->pkceCode;
+        }
+
         $params   = $grant->prepareRequestParameters($params, $options);
         $request  = $this->getAccessTokenRequest($params);
         $response = $this->getParsedResponse($request);
@@ -564,7 +662,7 @@ abstract class AbstractProvider
      *
      * @param  string $method
      * @param  string $url
-     * @param  AccessTokenInterface|string $token
+     * @param  AccessTokenInterface|string|null $token
      * @param  array $options Any of "headers", "body", and "protocolVersion".
      * @return RequestInterface
      */

+ 2 - 2
api/vendor/league/oauth2-client/src/Provider/Exception/IdentityProviderException.php

@@ -27,7 +27,7 @@ class IdentityProviderException extends \Exception
     /**
      * @param string $message
      * @param int $code
-     * @param array|string $response The response body
+     * @param mixed $response The response body
      */
     public function __construct($message, $code, $response)
     {
@@ -39,7 +39,7 @@ class IdentityProviderException extends \Exception
     /**
      * Returns the exception's response body.
      *
-     * @return array|string
+     * @return mixed
      */
     public function getResponseBody()
     {

+ 14 - 0
api/vendor/league/oauth2-client/src/Provider/GenericProvider.php

@@ -78,6 +78,11 @@ class GenericProvider extends AbstractProvider
      */
     private $responseResourceOwnerId = 'id';
 
+    /**
+     * @var string|null
+     */
+    private $pkceMethod = null;
+
     /**
      * @param array $options
      * @param array $collaborators
@@ -114,6 +119,7 @@ class GenericProvider extends AbstractProvider
             'responseCode',
             'responseResourceOwnerId',
             'scopes',
+            'pkceMethod',
         ]);
     }
 
@@ -205,6 +211,14 @@ class GenericProvider extends AbstractProvider
         return $this->scopeSeparator ?: parent::getScopeSeparator();
     }
 
+    /**
+     * @inheritdoc
+     */
+    protected function getPkceMethod()
+    {
+        return $this->pkceMethod ?: parent::getPkceMethod();
+    }
+
     /**
      * @inheritdoc
      */

+ 2 - 0
api/vendor/league/oauth2-client/src/Token/AccessTokenInterface.php

@@ -15,6 +15,7 @@
 namespace League\OAuth2\Client\Token;
 
 use JsonSerializable;
+use ReturnTypeWillChange;
 use RuntimeException;
 
 interface AccessTokenInterface extends JsonSerializable
@@ -68,5 +69,6 @@ interface AccessTokenInterface extends JsonSerializable
      *
      * @return array
      */
+    #[ReturnTypeWillChange]
     public function jsonSerialize();
 }

+ 1 - 1
api/vendor/league/oauth2-client/src/Tool/QueryBuilderTrait.php

@@ -28,6 +28,6 @@ trait QueryBuilderTrait
      */
     protected function buildQueryString(array $params)
     {
-        return http_build_query($params, null, '&', \PHP_QUERY_RFC3986);
+        return http_build_query($params, '', '&', \PHP_QUERY_RFC3986);
     }
 }

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 681 - 550
css/organizr.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1727 - 1228
js/functions.js


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است