mirror of
https://github.com/bitinflow/accounts.git
synced 2026-03-17 07:25:52 +00:00
change namespace and cleanup code
This commit is contained in:
17
src/Accounts/ApiOperations/Delete.php
Normal file
17
src/Accounts/ApiOperations/Delete.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitinflow\Accounts\ApiOperations;
|
||||
|
||||
use Bitinflow\Accounts\Helpers\Paginator;
|
||||
use Bitinflow\Accounts\Result;
|
||||
|
||||
/**
|
||||
* @author René Preuß <rene@preuss.io>
|
||||
*/
|
||||
trait Delete
|
||||
{
|
||||
|
||||
abstract public function delete(string $path = '', array $parameters = [], Paginator $paginator = null): Result;
|
||||
}
|
||||
17
src/Accounts/ApiOperations/Get.php
Normal file
17
src/Accounts/ApiOperations/Get.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitinflow\Accounts\ApiOperations;
|
||||
|
||||
use Bitinflow\Accounts\Helpers\Paginator;
|
||||
use Bitinflow\Accounts\Result;
|
||||
|
||||
/**
|
||||
* @author René Preuß <rene@preuss.io>
|
||||
*/
|
||||
trait Get
|
||||
{
|
||||
|
||||
abstract public function get(string $path = '', array $parameters = [], Paginator $paginator = null): Result;
|
||||
}
|
||||
17
src/Accounts/ApiOperations/Post.php
Normal file
17
src/Accounts/ApiOperations/Post.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitinflow\Accounts\ApiOperations;
|
||||
|
||||
use Bitinflow\Accounts\Helpers\Paginator;
|
||||
use Bitinflow\Accounts\Result;
|
||||
|
||||
/**
|
||||
* @author René Preuß <rene@preuss.io>
|
||||
*/
|
||||
trait Post
|
||||
{
|
||||
|
||||
abstract public function post(string $path = '', array $parameters = [], Paginator $paginator = null): Result;
|
||||
}
|
||||
17
src/Accounts/ApiOperations/Put.php
Normal file
17
src/Accounts/ApiOperations/Put.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitinflow\Accounts\ApiOperations;
|
||||
|
||||
use Bitinflow\Accounts\Helpers\Paginator;
|
||||
use Bitinflow\Accounts\Result;
|
||||
|
||||
/**
|
||||
* @author René Preuß <rene@preuss.io>
|
||||
*/
|
||||
trait Put
|
||||
{
|
||||
|
||||
abstract public function put(string $path = '', array $parameters = [], Paginator $paginator = null): Result;
|
||||
}
|
||||
82
src/Accounts/ApiTokenCookieFactory.php
Normal file
82
src/Accounts/ApiTokenCookieFactory.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Bitinflow\Accounts;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Firebase\JWT\JWT;
|
||||
use Illuminate\Contracts\Config\Repository as Config;
|
||||
use Illuminate\Contracts\Encryption\Encrypter;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
|
||||
class ApiTokenCookieFactory
|
||||
{
|
||||
/**
|
||||
* The configuration repository implementation.
|
||||
*
|
||||
* @var Config
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* The encrypter implementation.
|
||||
*
|
||||
* @var Encrypter
|
||||
*/
|
||||
protected $encrypter;
|
||||
|
||||
/**
|
||||
* Create an API token cookie factory instance.
|
||||
*
|
||||
* @param Config $config
|
||||
* @param Encrypter $encrypter
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Config $config, Encrypter $encrypter)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->encrypter = $encrypter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new API token cookie.
|
||||
*
|
||||
* @param mixed $userId
|
||||
* @param string $csrfToken
|
||||
* @return Cookie
|
||||
*/
|
||||
public function make($userId, string $csrfToken): Cookie
|
||||
{
|
||||
$config = $this->config->get('session');
|
||||
|
||||
$expiration = Carbon::now()->addMinutes($config['lifetime']);
|
||||
|
||||
return new Cookie(
|
||||
BitinflowAccounts::cookie(),
|
||||
$this->createToken($userId, $csrfToken, $expiration),
|
||||
$expiration,
|
||||
$config['path'],
|
||||
$config['domain'],
|
||||
$config['secure'],
|
||||
true,
|
||||
false,
|
||||
$config['same_site'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new JWT token for the given user ID and CSRF token.
|
||||
*
|
||||
* @param mixed $userId
|
||||
* @param string $csrfToken
|
||||
* @param Carbon $expiration
|
||||
* @return string
|
||||
*/
|
||||
protected function createToken($userId, string $csrfToken, Carbon $expiration): string
|
||||
{
|
||||
return JWT::encode([
|
||||
'sub' => $userId,
|
||||
'csrf' => $csrfToken,
|
||||
'expiry' => $expiration->getTimestamp(),
|
||||
], $this->encrypter->getKey());
|
||||
}
|
||||
}
|
||||
229
src/Accounts/Auth/TokenGuard.php
Normal file
229
src/Accounts/Auth/TokenGuard.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
namespace Bitinflow\Accounts\Auth;
|
||||
|
||||
use Bitinflow\Accounts\BitinflowAccounts;
|
||||
use Bitinflow\Accounts\Helpers\JwtParser;
|
||||
use Bitinflow\Accounts\Traits\HasBitinflowTokens;
|
||||
use Exception;
|
||||
use Firebase\JWT\JWT;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Auth\GuardHelpers;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Illuminate\Contracts\Debug\ExceptionHandler;
|
||||
use Illuminate\Contracts\Encryption\Encrypter;
|
||||
use Illuminate\Cookie\CookieValuePrefix;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Http\Request;
|
||||
use stdClass;
|
||||
use Throwable;
|
||||
|
||||
class TokenGuard
|
||||
{
|
||||
use GuardHelpers;
|
||||
|
||||
/**
|
||||
* @var Encrypter
|
||||
*/
|
||||
private $encrypter;
|
||||
|
||||
/**
|
||||
* @var JwtParser
|
||||
*/
|
||||
private $jwtParser;
|
||||
|
||||
public function __construct(UserProvider $provider, Encrypter $encrypter, JwtParser $jwtParser)
|
||||
{
|
||||
$this->provider = $provider;
|
||||
$this->encrypter = $encrypter;
|
||||
$this->jwtParser = $jwtParser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user for the incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
* @throws BindingResolutionException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function user(Request $request): ?Authenticatable
|
||||
{
|
||||
if ($request->bearerToken()) {
|
||||
return $this->authenticateViaBearerToken($request);
|
||||
} elseif ($request->cookie(BitinflowAccounts::cookie())) {
|
||||
return $this->authenticateViaCookie($request);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the incoming request via the Bearer token.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Authenticatable
|
||||
* @throws BindingResolutionException
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function authenticateViaBearerToken(Request $request): ?Authenticatable
|
||||
{
|
||||
if (!$token = $this->validateRequestViaBearerToken($request)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the access token is valid we will retrieve the user according to the user ID
|
||||
// associated with the token. We will use the provider implementation which may
|
||||
// be used to retrieve users from Eloquent. Next, we'll be ready to continue.
|
||||
/** @var Authenticatable|HasBitinflowTokens $user */
|
||||
$user = $this->provider->retrieveById(
|
||||
$request->attributes->get('oauth_user_id') ?: null
|
||||
);
|
||||
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $token ? $user->withBitinflowAccessToken($token) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate and get the incoming request via the Bearer token.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return stdClass|null
|
||||
* @throws BindingResolutionException
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function validateRequestViaBearerToken(Request $request): ?stdClass
|
||||
{
|
||||
try {
|
||||
$decoded = $this->jwtParser->decode($request);
|
||||
|
||||
$request->attributes->set('oauth_access_token_id', $decoded->jti);
|
||||
$request->attributes->set('oauth_client_id', $decoded->aud);
|
||||
$request->attributes->set('oauth_client_trusted', $decoded->client->trusted);
|
||||
$request->attributes->set('oauth_user_id', $decoded->sub);
|
||||
$request->attributes->set('oauth_scopes', $decoded->scopes);
|
||||
|
||||
return $decoded;
|
||||
} catch (AuthenticationException $e) {
|
||||
$request->headers->set('Authorization', '', true);
|
||||
|
||||
Container::getInstance()->make(
|
||||
ExceptionHandler::class
|
||||
)->report($e);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the incoming request via the token cookie.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
protected function authenticateViaCookie(Request $request)
|
||||
{
|
||||
if (!$token = $this->getTokenViaCookie($request)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If this user exists, we will return this user and attach a "transient" token to
|
||||
// the user model. The transient token assumes it has all scopes since the user
|
||||
// is physically logged into the application via the application's interface.
|
||||
/** @var Authenticatable|HasBitinflowTokens $user */
|
||||
if ($user = $this->provider->retrieveById($token['sub'])) {
|
||||
return $user->withBitinflowAccessToken((object)['scopes' => ['*']]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the token cookie via the incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array|null
|
||||
*/
|
||||
protected function getTokenViaCookie(Request $request): ?array
|
||||
{
|
||||
// If we need to retrieve the token from the cookie, it'll be encrypted so we must
|
||||
// first decrypt the cookie and then attempt to find the token value within the
|
||||
// database. If we can't decrypt the value we'll bail out with a null return.
|
||||
try {
|
||||
$token = $this->decodeJwtTokenCookie($request);
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We will compare the CSRF token in the decoded API token against the CSRF header
|
||||
// sent with the request. If they don't match then this request isn't sent from
|
||||
// a valid source and we won't authenticate the request for further handling.
|
||||
if (!BitinflowAccounts::$ignoreCsrfToken && (!$this->validCsrf($token, $request) ||
|
||||
time() >= $token['expiry'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode and decrypt the JWT token cookie.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
protected function decodeJwtTokenCookie(Request $request): array
|
||||
{
|
||||
return (array)JWT::decode(
|
||||
CookieValuePrefix::remove($this->encrypter->decrypt($request->cookie(BitinflowAccounts::cookie()), BitinflowAccounts::$unserializesCookies)),
|
||||
$this->encrypter->getKey(),
|
||||
['HS256']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the CSRF / header are valid and match.
|
||||
*
|
||||
* @param array $token
|
||||
* @param Request $request
|
||||
* @return bool
|
||||
*/
|
||||
protected function validCsrf(array $token, Request $request): bool
|
||||
{
|
||||
return isset($token['csrf']) && hash_equals(
|
||||
$token['csrf'], (string)$this->getTokenFromRequest($request)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CSRF token from the request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return string
|
||||
*/
|
||||
protected function getTokenFromRequest(Request $request): string
|
||||
{
|
||||
$token = $request->header('X-CSRF-TOKEN');
|
||||
|
||||
if (!$token && $header = $request->header('X-XSRF-TOKEN')) {
|
||||
$token = CookieValuePrefix::remove($this->encrypter->decrypt($header, static::serialized()));
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the cookie contents should be serialized.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function serialized(): bool
|
||||
{
|
||||
return EncryptCookies::serialized('XSRF-TOKEN');
|
||||
}
|
||||
}
|
||||
86
src/Accounts/Auth/UserProvider.php
Normal file
86
src/Accounts/Auth/UserProvider.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Bitinflow\Accounts\Auth;
|
||||
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Contracts\Auth\UserProvider as Base;
|
||||
|
||||
class UserProvider implements Base
|
||||
{
|
||||
/**
|
||||
* The user provider instance.
|
||||
*
|
||||
* @var Base
|
||||
*/
|
||||
protected $provider;
|
||||
|
||||
/**
|
||||
* The user provider name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $providerName;
|
||||
|
||||
/**
|
||||
* Create a new Bitinflow Accounts user provider.
|
||||
*
|
||||
* @param Base $provider
|
||||
* @param string $providerName
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Base $provider, $providerName)
|
||||
{
|
||||
$this->provider = $provider;
|
||||
$this->providerName = $providerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function retrieveById($identifier)
|
||||
{
|
||||
return $this->provider->retrieveById($identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function retrieveByToken($identifier, $token)
|
||||
{
|
||||
return $this->provider->retrieveByToken($identifier, $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function updateRememberToken(Authenticatable $user, $token)
|
||||
{
|
||||
$this->provider->updateRememberToken($user, $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function retrieveByCredentials(array $credentials)
|
||||
{
|
||||
return $this->provider->retrieveByCredentials($credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateCredentials(Authenticatable $user, array $credentials)
|
||||
{
|
||||
return $this->provider->validateCredentials($user, $credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the user provider.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getProviderName()
|
||||
{
|
||||
return $this->providerName;
|
||||
}
|
||||
}
|
||||
483
src/Accounts/BitinflowAccounts.php
Normal file
483
src/Accounts/BitinflowAccounts.php
Normal file
@@ -0,0 +1,483 @@
|
||||
<?php
|
||||
|
||||
namespace Bitinflow\Accounts;
|
||||
|
||||
use Bitinflow\Accounts\ApiOperations;
|
||||
use Bitinflow\Accounts\Exceptions\RequestRequiresAuthenticationException;
|
||||
use Bitinflow\Accounts\Exceptions\RequestRequiresClientIdException;
|
||||
use Bitinflow\Accounts\Exceptions\RequestRequiresRedirectUriException;
|
||||
use Bitinflow\Accounts\Helpers\Paginator;
|
||||
use Bitinflow\Accounts\Traits;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
/**
|
||||
* @author René Preuß <rene@preuss.io>
|
||||
*/
|
||||
class BitinflowAccounts
|
||||
{
|
||||
|
||||
use Traits\OauthTrait;
|
||||
use Traits\SshKeysTrait;
|
||||
use Traits\UsersTrait;
|
||||
|
||||
use Traits\HasBitinflowPaymentsWallet;
|
||||
|
||||
use ApiOperations\Delete;
|
||||
use ApiOperations\Get;
|
||||
use ApiOperations\Post;
|
||||
use ApiOperations\Put;
|
||||
|
||||
/**
|
||||
* The name for API token cookies.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $cookie = 'bitinflow_token';
|
||||
/**
|
||||
* Indicates if Bitinflow Accounts should ignore incoming CSRF tokens.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static $ignoreCsrfToken = false;
|
||||
/**
|
||||
* Indicates if Bitinflow Accounts should unserializes cookies.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static $unserializesCookies = false;
|
||||
private static $baseUrl = 'https://accounts.bitinflow.com/api/';
|
||||
/**
|
||||
* Guzzle is used to make http requests.
|
||||
*
|
||||
* @var Client
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* Paginator object.
|
||||
*
|
||||
* @var Paginator
|
||||
*/
|
||||
protected $paginator;
|
||||
|
||||
/**
|
||||
* bitinflow Accounts OAuth token.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $token = null;
|
||||
|
||||
/**
|
||||
* bitinflow Accounts client id.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $clientId = null;
|
||||
|
||||
/**
|
||||
* bitinflow Accounts client secret.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $clientSecret = null;
|
||||
|
||||
/**
|
||||
* bitinflow Accounts OAuth redirect url.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $redirectUri = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
if ($clientId = config('bitinflow-accounts-api.client_id')) {
|
||||
$this->setClientId($clientId);
|
||||
}
|
||||
if ($clientSecret = config('bitinflow-accounts-api.client_secret')) {
|
||||
$this->setClientSecret($clientSecret);
|
||||
}
|
||||
if ($redirectUri = config('bitinflow-accounts-api.redirect_url')) {
|
||||
$this->setRedirectUri($redirectUri);
|
||||
}
|
||||
if ($redirectUri = config('bitinflow-accounts-api.base_url')) {
|
||||
self::setBaseUrl($redirectUri);
|
||||
}
|
||||
$this->client = new Client([
|
||||
'base_uri' => self::$baseUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $baseUrl
|
||||
*
|
||||
* @internal only for internal and debug purposes.
|
||||
*/
|
||||
public static function setBaseUrl(string $baseUrl): void
|
||||
{
|
||||
self::$baseUrl = $baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set the name for API token cookies.
|
||||
*
|
||||
* @param string|null $cookie
|
||||
* @return string|static
|
||||
*/
|
||||
public static function cookie($cookie = null)
|
||||
{
|
||||
if (is_null($cookie)) {
|
||||
return static::$cookie;
|
||||
}
|
||||
|
||||
static::$cookie = $cookie;
|
||||
|
||||
return new static;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current user for the application with the given scopes.
|
||||
*
|
||||
* @param Authenticatable|Traits\HasBitinflowTokens $user
|
||||
* @param array $scopes
|
||||
* @param string $guard
|
||||
* @return Authenticatable
|
||||
*/
|
||||
public static function actingAs($user, $scopes = [], $guard = 'api')
|
||||
{
|
||||
$user->withBitinflowAccessToken((object)[
|
||||
'scopes' => $scopes
|
||||
]);
|
||||
|
||||
if (isset($user->wasRecentlyCreated) && $user->wasRecentlyCreated) {
|
||||
$user->wasRecentlyCreated = false;
|
||||
}
|
||||
|
||||
app('auth')->guard($guard)->setUser($user);
|
||||
|
||||
app('auth')->shouldUse($guard);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fluid client id setter.
|
||||
*
|
||||
* @param string $clientId bitinflow Accounts client id.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function withClientId(string $clientId): self
|
||||
{
|
||||
$this->setClientId($clientId);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client secret.
|
||||
*
|
||||
* @return string
|
||||
* @throws RequestRequiresClientIdException
|
||||
*/
|
||||
public function getClientSecret(): string
|
||||
{
|
||||
if (!$this->clientSecret) {
|
||||
throw new RequestRequiresClientIdException;
|
||||
}
|
||||
|
||||
return $this->clientSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set client secret.
|
||||
*
|
||||
* @param string $clientSecret bitinflow Accounts client secret
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setClientSecret(string $clientSecret): void
|
||||
{
|
||||
$this->clientSecret = $clientSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fluid client secret setter.
|
||||
*
|
||||
* @param string $clientSecret bitinflow Accounts client secret
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function withClientSecret(string $clientSecret): self
|
||||
{
|
||||
$this->setClientSecret($clientSecret);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get redirect url.
|
||||
*
|
||||
* @return string
|
||||
* @throws RequestRequiresRedirectUriException
|
||||
*/
|
||||
public function getRedirectUri(): string
|
||||
{
|
||||
if (!$this->redirectUri) {
|
||||
throw new RequestRequiresRedirectUriException;
|
||||
}
|
||||
|
||||
return $this->redirectUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set redirect url.
|
||||
*
|
||||
* @param string $redirectUri
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setRedirectUri(string $redirectUri): void
|
||||
{
|
||||
$this->redirectUri = $redirectUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fluid redirect url setter.
|
||||
*
|
||||
* @param string $redirectUri
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function withRedirectUri(string $redirectUri): self
|
||||
{
|
||||
$this->setRedirectUri($redirectUri);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OAuth token.
|
||||
*
|
||||
* @return string bitinflow Accounts token
|
||||
* @return string|null
|
||||
* @throws RequestRequiresAuthenticationException
|
||||
*/
|
||||
public function getToken()
|
||||
{
|
||||
if (!$this->token) {
|
||||
throw new RequestRequiresAuthenticationException;
|
||||
}
|
||||
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set OAuth token.
|
||||
*
|
||||
* @param string $token bitinflow Accounts OAuth token
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setToken(string $token): void
|
||||
{
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fluid OAuth token setter.
|
||||
*
|
||||
* @param string $token bitinflow Accounts OAuth token
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function withToken(string $token): self
|
||||
{
|
||||
$this->setToken($token);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param array $parameters
|
||||
* @param Paginator|null $paginator
|
||||
*
|
||||
* @return Result
|
||||
* @throws GuzzleException
|
||||
* @throws RequestRequiresClientIdException
|
||||
*/
|
||||
public function get(string $path = '', array $parameters = [], Paginator $paginator = null): Result
|
||||
{
|
||||
return $this->query('GET', $path, $parameters, $paginator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build query & execute.
|
||||
*
|
||||
* @param string $method HTTP method
|
||||
* @param string $path Query path
|
||||
* @param array $parameters Query parameters
|
||||
* @param Paginator $paginator Paginator object
|
||||
* @param mixed|null $jsonBody JSON data
|
||||
*
|
||||
* @return Result Result object
|
||||
* @throws GuzzleException
|
||||
* @throws RequestRequiresClientIdException
|
||||
*/
|
||||
public function query(string $method = 'GET', string $path = '', array $parameters = [], Paginator $paginator = null, $jsonBody = null): Result
|
||||
{
|
||||
if ($paginator !== null) {
|
||||
$parameters[$paginator->action] = $paginator->cursor();
|
||||
}
|
||||
try {
|
||||
$response = $this->client->request($method, $path, [
|
||||
'headers' => $this->buildHeaders($jsonBody ? true : false),
|
||||
'query' => $this->buildQuery($parameters),
|
||||
'json' => $jsonBody ?: null,
|
||||
]);
|
||||
$result = new Result($response, null, $paginator);
|
||||
} catch (RequestException $exception) {
|
||||
$result = new Result($exception->getResponse(), $exception, $paginator);
|
||||
}
|
||||
$result->bitinflow = $this;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build headers for request.
|
||||
*
|
||||
* @param bool $json Body is JSON
|
||||
*
|
||||
* @return array
|
||||
* @throws RequestRequiresClientIdException
|
||||
*/
|
||||
private function buildHeaders(bool $json = false): array
|
||||
{
|
||||
$headers = [
|
||||
'Client-ID' => $this->getClientId(),
|
||||
'Accept' => 'application/json',
|
||||
];
|
||||
if ($this->token) {
|
||||
$headers['Authorization'] = 'Bearer ' . $this->token;
|
||||
}
|
||||
if ($json) {
|
||||
$headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client id.
|
||||
*
|
||||
* @return string
|
||||
* @throws RequestRequiresClientIdException
|
||||
*/
|
||||
public function getClientId(): string
|
||||
{
|
||||
if (!$this->clientId) {
|
||||
throw new RequestRequiresClientIdException;
|
||||
}
|
||||
|
||||
return $this->clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set client id.
|
||||
*
|
||||
* @param string $clientId bitinflow Accounts client id
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setClientId(string $clientId): void
|
||||
{
|
||||
$this->clientId = $clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build query with support for multiple smae first-dimension keys.
|
||||
*
|
||||
* @param array $query
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buildQuery(array $query): string
|
||||
{
|
||||
$parts = [];
|
||||
foreach ($query as $name => $value) {
|
||||
$value = (array)$value;
|
||||
array_walk_recursive($value, function ($value) use (&$parts, $name) {
|
||||
$parts[] = urlencode($name) . '=' . urlencode($value);
|
||||
});
|
||||
}
|
||||
|
||||
return implode('&', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param array $parameters
|
||||
* @param Paginator|null $paginator
|
||||
*
|
||||
* @return Result
|
||||
* @throws GuzzleException
|
||||
* @throws RequestRequiresClientIdException
|
||||
*/
|
||||
public function post(string $path = '', array $parameters = [], Paginator $paginator = null): Result
|
||||
{
|
||||
return $this->query('POST', $path, $parameters, $paginator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param array $parameters
|
||||
* @param Paginator|null $paginator
|
||||
*
|
||||
* @return Result
|
||||
* @throws GuzzleException
|
||||
* @throws RequestRequiresClientIdException
|
||||
*/
|
||||
public function delete(string $path = '', array $parameters = [], Paginator $paginator = null): Result
|
||||
{
|
||||
return $this->query('DELETE', $path, $parameters, $paginator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param array $parameters
|
||||
* @param Paginator|null $paginator
|
||||
*
|
||||
* @return Result
|
||||
* @throws GuzzleException
|
||||
* @throws RequestRequiresClientIdException
|
||||
*/
|
||||
public function put(string $path = '', array $parameters = [], Paginator $paginator = null): Result
|
||||
{
|
||||
return $this->query('PUT', $path, $parameters, $paginator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $method
|
||||
* @param string $path
|
||||
* @param array|null $body
|
||||
*
|
||||
* @return Result
|
||||
* @throws GuzzleException
|
||||
* @throws RequestRequiresClientIdException
|
||||
*/
|
||||
public function json(string $method, string $path = '', array $body = null): Result
|
||||
{
|
||||
if ($body) {
|
||||
$body = json_encode(['data' => $body]);
|
||||
}
|
||||
|
||||
return $this->query($method, $path, [], null, $body);
|
||||
}
|
||||
}
|
||||
40
src/Accounts/Enums/Scope.php
Normal file
40
src/Accounts/Enums/Scope.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitinflow\Accounts\Enums;
|
||||
|
||||
/**
|
||||
* @author René Preuß <rene@preuss.io>
|
||||
*/
|
||||
class Scope
|
||||
{
|
||||
|
||||
/*
|
||||
* v0 API
|
||||
*/
|
||||
|
||||
// Deprecated scope.
|
||||
public const API = 'api';
|
||||
|
||||
// Read nonpublic user information, including email address.
|
||||
public const READ_USER = 'read_user';
|
||||
|
||||
/*
|
||||
* v1 API
|
||||
*/
|
||||
|
||||
// Read authorized user´s email address.
|
||||
public const USERS_READ_EMAIL = 'users:read:email';
|
||||
|
||||
// Manage a authorized user object.
|
||||
public const USERS_EDIT = 'users:edit';
|
||||
|
||||
public const USERS_CREATE = 'users:create';
|
||||
|
||||
// Read authorized user´s transactions.
|
||||
public const TRANSACTIONS_READ = 'transactions:read';
|
||||
|
||||
// Create a new charge for the authorized user.
|
||||
public const CHARGES_CREATE = 'charges:create';
|
||||
}
|
||||
41
src/Accounts/Exceptions/MissingScopeException.php
Normal file
41
src/Accounts/Exceptions/MissingScopeException.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Bitinflow\Accounts\Exceptions;
|
||||
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class MissingScopeException extends AuthorizationException
|
||||
{
|
||||
/**
|
||||
* The scopes that the user did not have.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $scopes;
|
||||
|
||||
/**
|
||||
* Create a new missing scope exception.
|
||||
*
|
||||
* @param array|string $scopes
|
||||
* @param string $message
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($scopes = [], $message = 'Invalid scope(s) provided.')
|
||||
{
|
||||
parent::__construct($message);
|
||||
|
||||
$this->scopes = Arr::wrap($scopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the scopes that the user did not have.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function scopes()
|
||||
{
|
||||
return $this->scopes;
|
||||
}
|
||||
}
|
||||
18
src/Accounts/Exceptions/RateLimitException.php
Normal file
18
src/Accounts/Exceptions/RateLimitException.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitinflow\Accounts\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* @author René Preuß <rene@preuss.io>
|
||||
*/
|
||||
class RateLimitException extends Exception
|
||||
{
|
||||
public function __construct($message = 'Rate Limit exceeded', $code = 0, Exception $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitinflow\Accounts\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* @author René Preuß <rene@preuss.io>
|
||||
*/
|
||||
class RequestRequiresAuthenticationException extends Exception
|
||||
{
|
||||
public function __construct($message = 'Request requires authentication', $code = 0, Exception $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
18
src/Accounts/Exceptions/RequestRequiresClientIdException.php
Normal file
18
src/Accounts/Exceptions/RequestRequiresClientIdException.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitinflow\Accounts\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* @author René Preuß <rene@preuss.io>
|
||||
*/
|
||||
class RequestRequiresClientIdException extends Exception
|
||||
{
|
||||
public function __construct($message = 'Request requires Client-ID', $code = 0, Exception $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
18
src/Accounts/Exceptions/RequestRequiresParameter.php
Normal file
18
src/Accounts/Exceptions/RequestRequiresParameter.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitinflow\Accounts\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* @author René Preuß <rene@preuss.io>
|
||||
*/
|
||||
class RequestRequiresParameter extends Exception
|
||||
{
|
||||
public function __construct($message = 'Request requires parameters', $code = 0, Exception $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitinflow\Accounts\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* @author René Preuß <rene@preuss.io>
|
||||
*/
|
||||
class RequestRequiresRedirectUriException extends Exception
|
||||
{
|
||||
public function __construct($message = 'Request requires redirect uri', $code = 0, Exception $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
20
src/Accounts/Facades/BitinflowAccounts.php
Normal file
20
src/Accounts/Facades/BitinflowAccounts.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitinflow\Accounts\Facades;
|
||||
|
||||
use Bitinflow\Accounts\BitinflowAccounts as BitinflowAccountsService;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
/**
|
||||
* @author René Preuß <rene@preuss.io>
|
||||
*/
|
||||
class BitinflowAccounts extends Facade
|
||||
{
|
||||
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return BitinflowAccountsService::class;
|
||||
}
|
||||
}
|
||||
41
src/Accounts/Helpers/JwtParser.php
Normal file
41
src/Accounts/Helpers/JwtParser.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Bitinflow\Accounts\Helpers;
|
||||
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Http\Request;
|
||||
use stdClass;
|
||||
use Throwable;
|
||||
|
||||
class JwtParser
|
||||
{
|
||||
public const ALLOWED_ALGORITHMS = ['RS256'];
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return stdClass
|
||||
* @throws AuthenticationException
|
||||
*/
|
||||
public function decode(Request $request): stdClass
|
||||
{
|
||||
JWT::$leeway = 60;
|
||||
|
||||
try {
|
||||
return JWT::decode(
|
||||
$request->bearerToken(),
|
||||
$this->getOauthPublicKey(),
|
||||
self::ALLOWED_ALGORITHMS
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
throw (new AuthenticationException());
|
||||
}
|
||||
}
|
||||
|
||||
private function getOauthPublicKey()
|
||||
{
|
||||
return file_get_contents(__DIR__ . '/../../../../oauth-public.key');
|
||||
}
|
||||
}
|
||||
96
src/Accounts/Helpers/Paginator.php
Normal file
96
src/Accounts/Helpers/Paginator.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitinflow\Accounts\Helpers;
|
||||
|
||||
use Bitinflow\Accounts\Result;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @author René Preuß <rene@preuss.io>
|
||||
*/
|
||||
class Paginator
|
||||
{
|
||||
|
||||
/**
|
||||
* Next desired action (first, after, before).
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
public $action = null;
|
||||
/**
|
||||
* bitinflow Accounts response pagination cursor.
|
||||
*
|
||||
* @var null|stdClass
|
||||
*/
|
||||
private $pagination;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param null|stdClass $pagination bitinflow Accounts response pagination cursor
|
||||
*/
|
||||
public function __construct(stdClass $pagination = null)
|
||||
{
|
||||
$this->pagination = $pagination;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Paginator from Result object.
|
||||
*
|
||||
* @param Result $result Result object
|
||||
*
|
||||
* @return self Paginator object
|
||||
*/
|
||||
public static function from(Result $result): self
|
||||
{
|
||||
return new self($result->pagination);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current active cursor.
|
||||
*
|
||||
* @return string bitinflow Accounts cursor
|
||||
*/
|
||||
public function cursor(): string
|
||||
{
|
||||
return $this->pagination->cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Paginator to fetch the next set of results.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function first(): self
|
||||
{
|
||||
$this->action = 'first';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Paginator to fetch the first set of results.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function next(): self
|
||||
{
|
||||
$this->action = 'after';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Paginator to fetch the last set of results.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function back(): self
|
||||
{
|
||||
$this->action = 'before';
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
32
src/Accounts/Http/Middleware/CheckClientCredentials.php
Normal file
32
src/Accounts/Http/Middleware/CheckClientCredentials.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Bitinflow\Accounts\Http\Middleware;
|
||||
|
||||
use Bitinflow\Accounts\Exceptions\MissingScopeException;
|
||||
use stdClass;
|
||||
|
||||
class CheckClientCredentials extends CheckCredentials
|
||||
{
|
||||
/**
|
||||
* Validate token credentials.
|
||||
*
|
||||
* @param stdClass $token
|
||||
* @param array $scopes
|
||||
*
|
||||
* @return void
|
||||
* @throws MissingScopeException
|
||||
*
|
||||
*/
|
||||
protected function validateScopes(stdClass $token, array $scopes)
|
||||
{
|
||||
if (in_array('*', $token->scopes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($scopes as $scope) {
|
||||
if (!in_array($scope, $token->scopes)) {
|
||||
throw new MissingScopeException($scopes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Bitinflow\Accounts\Http\Middleware;
|
||||
|
||||
use Bitinflow\Accounts\Exceptions\MissingScopeException;
|
||||
use stdClass;
|
||||
|
||||
class CheckClientCredentialsForAnyScope extends CheckCredentials
|
||||
{
|
||||
/**
|
||||
* Validate token credentials.
|
||||
*
|
||||
* @param stdClass $token
|
||||
* @param array $scopes
|
||||
*
|
||||
* @return void
|
||||
* @throws MissingScopeException
|
||||
*
|
||||
*/
|
||||
protected function validateScopes(stdClass $token, array $scopes)
|
||||
{
|
||||
if (in_array('*', $token->scopes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($scopes as $scope) {
|
||||
if (in_array($scope, $token->scopes)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new MissingScopeException($scopes);
|
||||
}
|
||||
}
|
||||
46
src/Accounts/Http/Middleware/CheckCredentials.php
Normal file
46
src/Accounts/Http/Middleware/CheckCredentials.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Bitinflow\Accounts\Http\Middleware;
|
||||
|
||||
use Bitinflow\Accounts\Exceptions\MissingScopeException;
|
||||
use Bitinflow\Accounts\Helpers\JwtParser;
|
||||
use Closure;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Http\Request;
|
||||
use stdClass;
|
||||
|
||||
abstract class CheckCredentials
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @param mixed ...$scopes
|
||||
*
|
||||
* @return mixed
|
||||
* @throws AuthenticationException|MissingScopeException
|
||||
*
|
||||
*/
|
||||
public function handle($request, Closure $next, ...$scopes)
|
||||
{
|
||||
$decoded = $this->getJwtParser()->decode($request);
|
||||
|
||||
$request->attributes->set('oauth_access_token_id', $decoded->jti);
|
||||
$request->attributes->set('oauth_client_id', $decoded->aud);
|
||||
//$request->attributes->set('oauth_client_trusted', $decoded->client->trusted);
|
||||
$request->attributes->set('oauth_user_id', $decoded->sub);
|
||||
$request->attributes->set('oauth_scopes', $decoded->scopes);
|
||||
|
||||
$this->validateScopes($decoded, $scopes);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
private function getJwtParser(): JwtParser
|
||||
{
|
||||
return app(JwtParser::class);
|
||||
}
|
||||
|
||||
abstract protected function validateScopes(stdClass $token, array $scopes);
|
||||
}
|
||||
39
src/Accounts/Http/Middleware/CheckForAnyScope.php
Normal file
39
src/Accounts/Http/Middleware/CheckForAnyScope.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Bitinflow\Accounts\Http\Middleware;
|
||||
|
||||
|
||||
use Bitinflow\Accounts\Exceptions\MissingScopeException;
|
||||
use Closure;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class CheckForAnyScope
|
||||
{
|
||||
/**
|
||||
* Handle the incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @param mixed ...$scopes
|
||||
* @return Response
|
||||
*
|
||||
* @throws AuthenticationException|MissingScopeException
|
||||
*/
|
||||
public function handle($request, $next, ...$scopes)
|
||||
{
|
||||
if (!$request->user() || !$request->user()->bitinflowToken()) {
|
||||
throw new AuthenticationException;
|
||||
}
|
||||
|
||||
foreach ($scopes as $scope) {
|
||||
if ($request->user()->bitinflowTokenCan($scope)) {
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
||||
throw new MissingScopeException($scopes);
|
||||
}
|
||||
}
|
||||
37
src/Accounts/Http/Middleware/CheckScopes.php
Normal file
37
src/Accounts/Http/Middleware/CheckScopes.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Bitinflow\Accounts\Http\Middleware;
|
||||
|
||||
use Bitinflow\Accounts\Exceptions\MissingScopeException;
|
||||
use Closure;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class CheckScopes
|
||||
{
|
||||
/**
|
||||
* Handle the incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @param mixed ...$scopes
|
||||
* @return Response
|
||||
*
|
||||
* @throws AuthenticationException|MissingScopeException
|
||||
*/
|
||||
public function handle($request, $next, ...$scopes)
|
||||
{
|
||||
if (!$request->user() || !$request->user()->bitinflowToken()) {
|
||||
throw new AuthenticationException;
|
||||
}
|
||||
|
||||
foreach ($scopes as $scope) {
|
||||
if (!$request->user()->bitinflowTokenCan($scope)) {
|
||||
throw new MissingScopeException($scope);
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
117
src/Accounts/Http/Middleware/CreateFreshApiToken.php
Normal file
117
src/Accounts/Http/Middleware/CreateFreshApiToken.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Bitinflow\Accounts\Http\Middleware;
|
||||
|
||||
use Bitinflow\Accounts\ApiTokenCookieFactory;
|
||||
use Bitinflow\Accounts\BitinflowAccounts;
|
||||
use Closure;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class CreateFreshApiToken
|
||||
{
|
||||
/**
|
||||
* The API token cookie factory instance.
|
||||
*
|
||||
* @var ApiTokenCookieFactory
|
||||
*/
|
||||
protected $cookieFactory;
|
||||
|
||||
/**
|
||||
* The authentication guard.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $guard;
|
||||
|
||||
/**
|
||||
* Create a new middleware instance.
|
||||
*
|
||||
* @param ApiTokenCookieFactory $cookieFactory
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(ApiTokenCookieFactory $cookieFactory)
|
||||
{
|
||||
$this->cookieFactory = $cookieFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @param string|null $guard
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next, $guard = null)
|
||||
{
|
||||
$this->guard = $guard;
|
||||
|
||||
$response = $next($request);
|
||||
|
||||
if ($this->shouldReceiveFreshToken($request, $response)) {
|
||||
$response->withCookie($this->cookieFactory->make(
|
||||
$request->user($this->guard)->getAuthIdentifier(), $request->session()->token()
|
||||
));
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given request should receive a fresh token.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @return bool
|
||||
*/
|
||||
protected function shouldReceiveFreshToken($request, $response): bool
|
||||
{
|
||||
return $this->requestShouldReceiveFreshToken($request) &&
|
||||
$this->responseShouldReceiveFreshToken($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the request should receive a fresh token.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return bool
|
||||
*/
|
||||
protected function requestShouldReceiveFreshToken($request)
|
||||
{
|
||||
return $request->isMethod('GET') && $request->user($this->guard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the response should receive a fresh token.
|
||||
*
|
||||
* @param Response $response
|
||||
* @return bool
|
||||
*/
|
||||
protected function responseShouldReceiveFreshToken($response)
|
||||
{
|
||||
return ($response instanceof Response ||
|
||||
$response instanceof JsonResponse) &&
|
||||
!$this->alreadyContainsToken($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given response already contains an API token.
|
||||
*
|
||||
* This avoids us overwriting a just "refreshed" token.
|
||||
*
|
||||
* @param Response $response
|
||||
* @return bool
|
||||
*/
|
||||
protected function alreadyContainsToken($response): bool
|
||||
{
|
||||
foreach ($response->headers->getCookies() as $cookie) {
|
||||
if ($cookie->getName() === BitinflowAccounts::cookie()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
87
src/Accounts/Providers/BitinflowAccountsServiceProvider.php
Normal file
87
src/Accounts/Providers/BitinflowAccountsServiceProvider.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitinflow\Accounts\Providers;
|
||||
|
||||
use Bitinflow\Accounts\Auth\TokenGuard;
|
||||
use Bitinflow\Accounts\Auth\UserProvider;
|
||||
use Bitinflow\Accounts\BitinflowAccounts;
|
||||
use Bitinflow\Accounts\Helpers\JwtParser;
|
||||
use Illuminate\Auth\RequestGuard;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class BitinflowAccountsServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
||||
/**
|
||||
* Bootstrap the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->publishes([
|
||||
dirname(__DIR__, 4) . '/config/bitinflow-accounts.php' => config_path('bitinflow-accounts.php'),
|
||||
], 'config');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->mergeConfigFrom(dirname(__DIR__, 4) . '/config/bitinflow-accounts.php', 'bitinflow-accounts');
|
||||
$this->app->singleton(BitinflowAccounts::class, function () {
|
||||
return new BitinflowAccounts;
|
||||
});
|
||||
|
||||
$this->registerGuard();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the token guard.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerGuard()
|
||||
{
|
||||
Auth::resolved(function ($auth) {
|
||||
$auth->extend('bitinflow-accounts', function ($app, $name, array $config) {
|
||||
return tap($this->makeGuard($config), function ($guard) {
|
||||
$this->app->refresh('request', $guard, 'setRequest');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an instance of the token guard.
|
||||
*
|
||||
* @param array $config
|
||||
* @return RequestGuard
|
||||
*/
|
||||
protected function makeGuard(array $config): RequestGuard
|
||||
{
|
||||
return new RequestGuard(function ($request) use ($config) {
|
||||
return (new TokenGuard(
|
||||
new UserProvider(Auth::createUserProvider($config['provider']), $config['provider']),
|
||||
$this->app->make('encrypter'),
|
||||
$this->app->make(JwtParser::class)
|
||||
))->user($request);
|
||||
}, $this->app['request']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function provides()
|
||||
{
|
||||
return [BitinflowAccounts::class];
|
||||
}
|
||||
}
|
||||
125
src/Accounts/Providers/BitinflowAccountsSsoUserProvider.php
Normal file
125
src/Accounts/Providers/BitinflowAccountsSsoUserProvider.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitinflow\Accounts\Providers;
|
||||
|
||||
use Bitinflow\Accounts\BitinflowAccounts;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Contracts\Auth\UserProvider;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class BitinflowAccountsSsoUserProvider implements UserProvider
|
||||
{
|
||||
private $bitinflowAccounts;
|
||||
private $accessTokenField = null;
|
||||
private $fields;
|
||||
private $model;
|
||||
private $request;
|
||||
|
||||
public function __construct(
|
||||
BitinflowAccounts $bitinflowAccounts,
|
||||
Request $request,
|
||||
string $model,
|
||||
array $fields,
|
||||
?string $accessTokenField = null
|
||||
) {
|
||||
$this->request = $request;
|
||||
$this->model = $model;
|
||||
$this->fields = $fields;
|
||||
$this->accessTokenField = $accessTokenField;
|
||||
$this->bitinflowAccounts = $bitinflowAccounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $identifier
|
||||
* @return Builder|Model|object|null
|
||||
*/
|
||||
public function retrieveById($identifier)
|
||||
{
|
||||
$model = $this->createModel();
|
||||
$token = $this->request->bearerToken();
|
||||
|
||||
$user = $this->newModelQuery($model)
|
||||
->where($model->getAuthIdentifierName(), $identifier)
|
||||
->first();
|
||||
|
||||
// Update access token when updated
|
||||
if ($this->accessTokenField) {
|
||||
$user[$this->accessTokenField] = $token;
|
||||
|
||||
if ($user->isDirty()) {
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
|
||||
if ($user) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$this->bitinflowAccounts->setToken($token);
|
||||
|
||||
$result = $this->bitinflowAccounts->getAuthedUser();
|
||||
|
||||
if (!$result->success()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$attributes = Arr::only((array)$result->data(), $this->fields);
|
||||
$attributes[$model->getAuthIdentifierName()] = $result->data->id;
|
||||
|
||||
if ($this->accessTokenField) {
|
||||
$attributes[$this->accessTokenField] = $token;
|
||||
}
|
||||
|
||||
return $this->newModelQuery($model)->create($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of the model.
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
public function createModel(): Model
|
||||
{
|
||||
$class = '\\' . ltrim($this->model, '\\');
|
||||
|
||||
return new $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new query builder for the model instance.
|
||||
*
|
||||
* @param Model|null $model
|
||||
* @return Builder
|
||||
*/
|
||||
protected function newModelQuery(Model $model = null): Builder
|
||||
{
|
||||
return is_null($model)
|
||||
? $this->createModel()->newQuery()
|
||||
: $model->newQuery();
|
||||
}
|
||||
|
||||
public function retrieveByToken($identifier, $token)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function updateRememberToken(Authenticatable $user, $token)
|
||||
{
|
||||
// void
|
||||
}
|
||||
|
||||
public function retrieveByCredentials(array $credentials)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function validateCredentials(Authenticatable $user, array $credentials)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
268
src/Accounts/Result.php
Normal file
268
src/Accounts/Result.php
Normal file
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitinflow\Accounts;
|
||||
|
||||
use Bitinflow\Accounts\Helpers\Paginator;
|
||||
use Exception;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use stdClass;
|
||||
|
||||
|
||||
/**
|
||||
* @author René Preuß <rene@preuss.io>
|
||||
*/
|
||||
class Result
|
||||
{
|
||||
|
||||
/**
|
||||
* Query successfull.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $success = false;
|
||||
|
||||
/**
|
||||
* Guzzle exception, if present.
|
||||
*
|
||||
* @var null|mixed
|
||||
*/
|
||||
public $exception = null;
|
||||
|
||||
/**
|
||||
* Query result data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $data = [];
|
||||
|
||||
/**
|
||||
* Total amount of result data.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $total = 0;
|
||||
|
||||
/**
|
||||
* Status Code.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $status = 0;
|
||||
|
||||
/**
|
||||
* bitinflow Accounts response pagination cursor.
|
||||
*
|
||||
* @var null|stdClass
|
||||
*/
|
||||
public $pagination;
|
||||
|
||||
/**
|
||||
* Internal paginator.
|
||||
*
|
||||
* @var null|Paginator
|
||||
*/
|
||||
public $paginator;
|
||||
|
||||
/**
|
||||
* Original Guzzle HTTP Response.
|
||||
*
|
||||
* @var ResponseInterface|null
|
||||
*/
|
||||
public $response;
|
||||
|
||||
/**
|
||||
* Original bitinflow Accounts instance.
|
||||
*
|
||||
* @var BitinflowAccounts
|
||||
*/
|
||||
public $bitinflow;
|
||||
|
||||
/**
|
||||
* Constructor,
|
||||
*
|
||||
* @param ResponseInterface|null $response HTTP response
|
||||
* @param Exception|mixed $exception Exception, if present
|
||||
* @param null|Paginator $paginator Paginator, if present
|
||||
*/
|
||||
public function __construct(?ResponseInterface $response, Exception $exception = null, Paginator $paginator = null)
|
||||
{
|
||||
$this->response = $response;
|
||||
$this->success = $exception === null;
|
||||
$this->exception = $exception;
|
||||
$this->status = $response ? $response->getStatusCode() : 500;
|
||||
$jsonResponse = $response ? @json_decode($response->getBody()->getContents(), false) : null;
|
||||
if ($jsonResponse !== null) {
|
||||
$this->setProperty($jsonResponse, 'data');
|
||||
$this->setProperty($jsonResponse, 'total');
|
||||
$this->setProperty($jsonResponse, 'pagination');
|
||||
$this->paginator = Paginator::from($this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a class attribute by given JSON Response Body.
|
||||
*
|
||||
* @param stdClass $jsonResponse Response Body
|
||||
* @param string $responseProperty Response property name
|
||||
* @param string|null $attribute Class property name
|
||||
*/
|
||||
private function setProperty(stdClass $jsonResponse, string $responseProperty, string $attribute = null): void
|
||||
{
|
||||
$classAttribute = $attribute ?? $responseProperty;
|
||||
if ($jsonResponse !== null && property_exists($jsonResponse, $responseProperty)) {
|
||||
$this->{$classAttribute} = $jsonResponse->{$responseProperty};
|
||||
} elseif ($responseProperty === 'data') {
|
||||
$this->{$classAttribute} = $jsonResponse;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns wether the query was successfull.
|
||||
*
|
||||
* @return bool Success state
|
||||
*/
|
||||
public function success(): bool
|
||||
{
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response data, also available as public attribute.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function data()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last HTTP or API error.
|
||||
*
|
||||
* @return string Error message
|
||||
*/
|
||||
public function error(): string
|
||||
{
|
||||
// TODO Switch Exception response parsing to this->data
|
||||
if ($this->exception === null || !$this->exception->hasResponse()) {
|
||||
return 'bitinflow Accounts API Unavailable';
|
||||
}
|
||||
$exception = (string)$this->exception->getResponse()->getBody();
|
||||
$exception = @json_decode($exception);
|
||||
if (property_exists($exception, 'message') && !empty($exception->message)) {
|
||||
return $exception->message;
|
||||
}
|
||||
|
||||
return $this->exception->getMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shifts the current result (Use for single user/video etc. query).
|
||||
*
|
||||
* @return mixed Shifted data
|
||||
*/
|
||||
public function shift()
|
||||
{
|
||||
if (!empty($this->data)) {
|
||||
$data = $this->data;
|
||||
|
||||
return array_shift($data);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current count of items in dataset.
|
||||
*
|
||||
* @return int Count
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count((array)$this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Paginator to fetch the next set of results.
|
||||
*
|
||||
* @return null|Paginator
|
||||
*/
|
||||
public function next(): ?Paginator
|
||||
{
|
||||
return $this->paginator !== null ? $this->paginator->next() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Paginator to fetch the last set of results.
|
||||
*
|
||||
* @return null|Paginator
|
||||
*/
|
||||
public function back(): ?Paginator
|
||||
{
|
||||
return $this->paginator !== null ? $this->paginator->back() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rate limit information.
|
||||
*
|
||||
* @param string|null $key Get defined index
|
||||
*
|
||||
* @return string|array|null
|
||||
*/
|
||||
public function rateLimit(string $key = null)
|
||||
{
|
||||
if (!$this->response) {
|
||||
return null;
|
||||
}
|
||||
$rateLimit = [
|
||||
'limit' => (int)$this->response->getHeaderLine('X-RateLimit-Limit'),
|
||||
'remaining' => (int)$this->response->getHeaderLine('X-RateLimit-Remaining'),
|
||||
'reset' => (int)$this->response->getHeaderLine('Retry-After'),
|
||||
];
|
||||
if ($key === null) {
|
||||
return $rateLimit;
|
||||
}
|
||||
|
||||
return $rateLimit[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert users in data response.
|
||||
*
|
||||
* @param string $identifierAttribute Attribute to identify the users
|
||||
* @param string $insertTo Data index to insert user data
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function insertUsers(string $identifierAttribute = 'user_id', string $insertTo = 'user'): self
|
||||
{
|
||||
$data = $this->data;
|
||||
$userIds = collect($data)->map(function ($item) use ($identifierAttribute) {
|
||||
return $item->{$identifierAttribute};
|
||||
})->toArray();
|
||||
if (count($userIds) === 0) {
|
||||
return $this;
|
||||
}
|
||||
$users = collect($this->bitinflow->getUsersByIds($userIds)->data);
|
||||
$dataWithUsers = collect($data)->map(function ($item) use ($users, $identifierAttribute, $insertTo) {
|
||||
$item->$insertTo = $users->where('id', $item->{$identifierAttribute})->first();
|
||||
|
||||
return $item;
|
||||
});
|
||||
$this->data = $dataWithUsers->toArray();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Paginator to fetch the first set of results.
|
||||
*
|
||||
* @return null|Paginator
|
||||
*/
|
||||
public function first(): ?Paginator
|
||||
{
|
||||
return $this->paginator !== null ? $this->paginator->first() : null;
|
||||
}
|
||||
}
|
||||
24
src/Accounts/Socialite/BitinflowExtendSocialite.php
Normal file
24
src/Accounts/Socialite/BitinflowExtendSocialite.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Bitinflow\Accounts\Socialite;
|
||||
|
||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||
|
||||
/**
|
||||
* @author René Preuß <rene@preuss.io>
|
||||
*/
|
||||
class BitinflowExtendSocialite
|
||||
{
|
||||
|
||||
/**
|
||||
* Register the provider.
|
||||
*
|
||||
* @param SocialiteWasCalled $socialiteWasCalled
|
||||
*/
|
||||
public function handle(SocialiteWasCalled $socialiteWasCalled)
|
||||
{
|
||||
$socialiteWasCalled->extendSocialite(
|
||||
'bitinflow-accounts', __NAMESPACE__ . '\Provider'
|
||||
);
|
||||
}
|
||||
}
|
||||
89
src/Accounts/Socialite/Provider.php
Normal file
89
src/Accounts/Socialite/Provider.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Bitinflow\Accounts\Socialite;
|
||||
|
||||
use Bitinflow\Accounts\Enums\Scope;
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Socialite\Two\ProviderInterface;
|
||||
use SocialiteProviders\Manager\OAuth2\AbstractProvider;
|
||||
use SocialiteProviders\Manager\OAuth2\User;
|
||||
|
||||
/**
|
||||
* @author René Preuß <rene@preuss.io>
|
||||
*/
|
||||
class Provider extends AbstractProvider implements ProviderInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Unique Provider Identifier.
|
||||
*/
|
||||
const IDENTIFIER = 'BITINFLOW_ACCOUNTS';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $scopes = [Scope::READ_USER];
|
||||
|
||||
/**
|
||||
* {@inherticdoc}.
|
||||
*/
|
||||
protected $scopeSeparator = ' ';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getAuthUrl($state)
|
||||
{
|
||||
return $this->buildAuthUrlFromBase(
|
||||
'https://accounts.bitinflow.com/oauth/authorize', $state
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getTokenUrl()
|
||||
{
|
||||
return 'https://accounts.bitinflow.com/oauth/token';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getUserByToken($token)
|
||||
{
|
||||
$response = $this->getHttpClient()->get(
|
||||
'https://accounts.bitinflow.com/api/user', [
|
||||
'headers' => [
|
||||
'Accept' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
],
|
||||
]);
|
||||
|
||||
return json_decode($response->getBody()->getContents(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function mapUserToObject(array $user)
|
||||
{
|
||||
return (new User())->setRaw($user)->map([
|
||||
'id' => $user['id'],
|
||||
'nickname' => $user['name'],
|
||||
'name' => $user['name'],
|
||||
'email' => Arr::get($user, 'email'),
|
||||
'avatar' => $user['avatar'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getTokenFields($code)
|
||||
{
|
||||
return array_merge(parent::getTokenFields($code), [
|
||||
'grant_type' => 'authorization_code',
|
||||
]);
|
||||
}
|
||||
}
|
||||
197
src/Accounts/Traits/HasBitinflowPaymentsWallet.php
Normal file
197
src/Accounts/Traits/HasBitinflowPaymentsWallet.php
Normal file
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
namespace Bitinflow\Accounts\Traits;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
|
||||
/**
|
||||
* @property string access_token todo: can we get this from HasBitinflowTokens ?
|
||||
* @property PendingRequest $paymentsGatewayUser
|
||||
*/
|
||||
trait HasBitinflowPaymentsWallet
|
||||
{
|
||||
protected $paymentsUser = null;
|
||||
|
||||
/**
|
||||
* Check if user has an active wallet.
|
||||
*
|
||||
* @return bool
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function hasWallet(): bool
|
||||
{
|
||||
return $this->getPaymentsUser()->data->has_wallet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user from payments gateway.
|
||||
*
|
||||
* @return object|null
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function getPaymentsUser(): ?object
|
||||
{
|
||||
if (is_null($this->paymentsUser)) {
|
||||
$this->paymentsUser = $this->paymentsGatewayRequest('GET', 'user');
|
||||
}
|
||||
|
||||
return $this->paymentsUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new payment gateway request.
|
||||
*
|
||||
* @param string $method
|
||||
* @param string $url
|
||||
* @param array $attributes
|
||||
* @return mixed
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
private function paymentsGatewayRequest(string $method, string $url, array $attributes = []): mixed
|
||||
{
|
||||
$client = new Client([
|
||||
'base_uri' => config('bitinflow-accounts.payments.base_url'),
|
||||
]);
|
||||
|
||||
$response = $client->request($method, $url, [
|
||||
RequestOptions::JSON => $attributes,
|
||||
RequestOptions::HEADERS => [
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => sprintf('Bearer %s', $this->access_token),
|
||||
],
|
||||
]);
|
||||
|
||||
return json_decode($response->getBody());
|
||||
}
|
||||
|
||||
public function getWalletSetupIntent(string $success_path = ''): string
|
||||
{
|
||||
return sprintf('%swallet?continue_url=%s', config('bitinflow-accounts.payments.dashboard_url'), url($success_path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get balance from user.
|
||||
*
|
||||
* @return float
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function getBalance(): float
|
||||
{
|
||||
return $this->getPaymentsUser()->data->balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get vat from user.
|
||||
*
|
||||
* @return int|null
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function getVat(): ?int
|
||||
{
|
||||
return $this->getPaymentsUser()->data->taxation->vat;
|
||||
}
|
||||
|
||||
public function hasSubscribed($name = 'default'): bool
|
||||
{
|
||||
$subscription = $this->getSubscription($name);
|
||||
|
||||
return $subscription && $subscription->status === 'settled' || $subscription && $subscription->resumeable;
|
||||
}
|
||||
|
||||
public function getSubscription($name = 'default'): ?object
|
||||
{
|
||||
foreach ($this->getSubscriptions() as $subscription) {
|
||||
if (isset($subscription->payload->name) && $subscription->payload->name === $name) {
|
||||
return $subscription;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get vat from user.
|
||||
*
|
||||
* @return array|null
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function getSubscriptions(): ?array
|
||||
{
|
||||
$subscriptions = $this->getPaymentsUser()->data->subscriptions;
|
||||
|
||||
foreach ($subscriptions as $key => $subscription) {
|
||||
if (!isset($subscription->payload->client_id) || $subscription->payload->client_id !== config('bitinflow-accounts.client_id')) {
|
||||
unset($subscriptions[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return $subscriptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new subscription.
|
||||
*
|
||||
* @param array $attributes array which requires following attributes:
|
||||
* name, description, period, price
|
||||
* and following attributes are optional:
|
||||
* vat, payload, ends_at, webhook_url, webhook_secret
|
||||
* @param array $payload optional data that is stored in the subscription
|
||||
* @param bool $checkout optional checkout it directly
|
||||
* @return object the subscription object
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function createSubscription(string $name, array $attributes, array $payload = [], bool $checkout = false): object
|
||||
{
|
||||
$client = [
|
||||
'name' => $name,
|
||||
'client_id' => config('bitinflow-accounts.client_id')
|
||||
];
|
||||
$defaults = ['period' => 'monthly'];
|
||||
$attributes = array_merge(array_merge($defaults, $attributes), [
|
||||
'payload' => array_merge($payload, $client),
|
||||
'checkout' => $checkout
|
||||
]);
|
||||
|
||||
return $this->paymentsGatewayRequest('POST', 'subscriptions', $attributes)->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkout given subscription.
|
||||
*
|
||||
* @param string $id
|
||||
* @return void
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function checkoutSubscription(string $id): void
|
||||
{
|
||||
$this->paymentsGatewayRequest('PUT', sprintf('subscriptions/%s/checkout', $id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke a running subscription.
|
||||
*
|
||||
* @param $id
|
||||
* @return void
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function revokeSubscription($id): void
|
||||
{
|
||||
$this->paymentsGatewayRequest('PUT', sprintf('subscriptions/%s/revoke', $id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume a running subscription.
|
||||
*
|
||||
* @param $id
|
||||
* @return void
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function resumeSubscription($id): void
|
||||
{
|
||||
$this->paymentsGatewayRequest('PUT', sprintf('subscriptions/%s/resume', $id));
|
||||
}
|
||||
}
|
||||
51
src/Accounts/Traits/HasBitinflowTokens.php
Normal file
51
src/Accounts/Traits/HasBitinflowTokens.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Bitinflow\Accounts\Traits;
|
||||
|
||||
use stdClass;
|
||||
|
||||
trait HasBitinflowTokens
|
||||
{
|
||||
/**
|
||||
* The current access token for the authentication user.
|
||||
*
|
||||
* @var stdClass
|
||||
*/
|
||||
protected $accessToken;
|
||||
|
||||
/**
|
||||
* Get the current access token being used by the user.
|
||||
*
|
||||
* @return stdClass|null
|
||||
*/
|
||||
public function bitinflowToken(): ?stdClass
|
||||
{
|
||||
return $this->accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current API token has a given scope.
|
||||
*
|
||||
* @param string $scope
|
||||
* @return bool
|
||||
*/
|
||||
public function bitinflowTokenCan(string $scope): bool
|
||||
{
|
||||
$scopes = $this->accessToken ? $this->accessToken->scopes : [];
|
||||
|
||||
return in_array('*', $scopes) || in_array($scope, $this->accessToken->scopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current access token for the user.
|
||||
*
|
||||
* @param stdClass $accessToken
|
||||
* @return $this
|
||||
*/
|
||||
public function withBitinflowAccessToken(stdClass $accessToken): self
|
||||
{
|
||||
$this->accessToken = $accessToken;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
44
src/Accounts/Traits/OauthTrait.php
Normal file
44
src/Accounts/Traits/OauthTrait.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitinflow\Accounts\Traits;
|
||||
|
||||
use Bitinflow\Accounts\Result;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
|
||||
/**
|
||||
* @author René Preuß <rene@preuss.io>
|
||||
*/
|
||||
trait OauthTrait
|
||||
{
|
||||
|
||||
/**
|
||||
* Retrieving a oauth token using a given grant type.
|
||||
*
|
||||
* @param string $grantType
|
||||
* @param array $attributes
|
||||
*
|
||||
* @return Result
|
||||
*/
|
||||
public function retrievingToken(string $grantType, array $attributes): Result
|
||||
{
|
||||
try {
|
||||
$response = $this->client->request('POST', '/oauth/token', [
|
||||
'form_params' => $attributes + [
|
||||
'grant_type' => $grantType,
|
||||
'client_id' => $this->getClientId(),
|
||||
'client_secret' => $this->getClientSecret(),
|
||||
],
|
||||
]);
|
||||
|
||||
$result = new Result($response, null);
|
||||
} catch (RequestException $exception) {
|
||||
$result = new Result($exception->getResponse(), $exception);
|
||||
}
|
||||
|
||||
$result->bitinflow = $this;
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
56
src/Accounts/Traits/SshKeysTrait.php
Normal file
56
src/Accounts/Traits/SshKeysTrait.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitinflow\Accounts\Traits;
|
||||
|
||||
use Bitinflow\Accounts\ApiOperations\Delete;
|
||||
use Bitinflow\Accounts\ApiOperations\Get;
|
||||
use Bitinflow\Accounts\ApiOperations\Post;
|
||||
use Bitinflow\Accounts\Result;
|
||||
|
||||
trait SshKeysTrait
|
||||
{
|
||||
|
||||
use Get, Post, Delete;
|
||||
|
||||
/**
|
||||
* Get currently authed user with Bearer Token
|
||||
*
|
||||
* @param int $id
|
||||
*
|
||||
* @return Result Result object
|
||||
*/
|
||||
public function getSshKeysByUserId(int $id): Result
|
||||
{
|
||||
return $this->get("users/$id/keys/json", [], null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates ssh key for the currently authed user
|
||||
*
|
||||
* @param string $publicKey
|
||||
* @param string|null $name
|
||||
*
|
||||
* @return Result Result object
|
||||
*/
|
||||
public function createSshKey(string $publicKey, string $name = null): Result
|
||||
{
|
||||
return $this->post('ssh-keys', [
|
||||
'public_key' => $publicKey,
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a given ssh key for the currently authed user
|
||||
*
|
||||
* @param int $id
|
||||
*
|
||||
* @return Result Result object
|
||||
*/
|
||||
public function deleteSshKey(int $id): Result
|
||||
{
|
||||
return $this->delete("ssh-keys/$id", []);
|
||||
}
|
||||
}
|
||||
50
src/Accounts/Traits/UsersTrait.php
Normal file
50
src/Accounts/Traits/UsersTrait.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitinflow\Accounts\Traits;
|
||||
|
||||
use Bitinflow\Accounts\ApiOperations\Get;
|
||||
use Bitinflow\Accounts\Result;
|
||||
|
||||
trait UsersTrait
|
||||
{
|
||||
|
||||
use Get;
|
||||
|
||||
/**
|
||||
* Get currently authed user with Bearer Token
|
||||
*
|
||||
* @return Result Result object
|
||||
*/
|
||||
public function getAuthedUser(): Result
|
||||
{
|
||||
return $this->get('users/me');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user on behalf of the current user.
|
||||
*
|
||||
* @param array $parameters
|
||||
*
|
||||
* @return Result
|
||||
*/
|
||||
public function createUser(array $parameters): Result
|
||||
{
|
||||
return $this->post('v2/users', $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given email exists.
|
||||
*
|
||||
* @param string $email
|
||||
*
|
||||
* @return Result
|
||||
*/
|
||||
public function isEmailExisting(string $email): Result
|
||||
{
|
||||
return $this->post('v2/users/check-email', [
|
||||
'email' => $email,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user