mirror of
https://github.com/anikeen-com/id.git
synced 2026-03-13 13:46:13 +00:00
first commit
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/vendor
|
||||||
|
.phpunit.result.cache
|
||||||
|
.idea
|
||||||
|
composer.lock
|
||||||
269
README.md
Normal file
269
README.md
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
# Anikeen ID
|
||||||
|
|
||||||
|
[](https://packagist.org/packages/anikeen/id)
|
||||||
|
[](https://packagist.org/packages/anikeen/id)
|
||||||
|
[](https://packagist.org/packages/anikeen/id)
|
||||||
|
|
||||||
|
PHP Anikeen ID API Client for Laravel 10+
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
1. [Installation](#installation)
|
||||||
|
2. [Event Listener](#event-listener)
|
||||||
|
3. [Configuration](#configuration)
|
||||||
|
4. [Examples](#examples)
|
||||||
|
5. [Documentation](#documentation)
|
||||||
|
6. [Development](#Development)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
composer require anikeen/id
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Listener
|
||||||
|
|
||||||
|
- Add `SocialiteProviders\Manager\SocialiteWasCalled` event to your `listen[]` array in `app/Providers/EventServiceProvider`.
|
||||||
|
- Add your listeners (i.e. the ones from the providers) to the `SocialiteProviders\Manager\SocialiteWasCalled[]` that you just created.
|
||||||
|
- The listener that you add for this provider is `'Anikeen\\Id\\Socialite\\AnikeenIdExtendSocialite@handle',`.
|
||||||
|
- Note: You do not need to add anything for the built-in socialite providers unless you override them with your own providers.
|
||||||
|
|
||||||
|
```
|
||||||
|
/**
|
||||||
|
* The event handler mappings for the application.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $listen = [
|
||||||
|
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
|
||||||
|
// add your listeners (aka providers) here
|
||||||
|
'Anikeen\\Id\\Socialite\\AnikeenIdExtendSocialite@handle',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Copy configuration to config folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ php artisan vendor:publish --provider="Anikeen\Id\Providers\AnikeenIdServiceProvider"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add environmental variables to your `.env`
|
||||||
|
|
||||||
|
```
|
||||||
|
ANIKEEN_ID_KEY=
|
||||||
|
ANIKEEN_ID_SECRET=
|
||||||
|
ANIKEEN_ID_REDIRECT_URI=http://localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
You will need to add an entry to the services configuration file so that after config files are cached for usage in production environment (Laravel command `artisan config:cache`) all config is still available.
|
||||||
|
|
||||||
|
**Add to `config/services.php`:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
'anikeen-id' => [
|
||||||
|
'client_id' => env('ANIKEEN_ID_KEY'),
|
||||||
|
'client_secret' => env('ANIKEEN_ID_SECRET'),
|
||||||
|
'redirect' => env('ANIKEEN_ID_REDIRECT_URI')
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementing Auth
|
||||||
|
|
||||||
|
This method should typically be called in the `boot` method of your `AuthServiceProvider` class:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Anikeen\Id\AnikeenId;
|
||||||
|
use Anikeen\Id\Providers\AnikeenIdSsoUserProvider;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register any authentication / authorization services.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function boot()
|
||||||
|
{
|
||||||
|
Auth::provider('sso-users', function ($app, array $config) {
|
||||||
|
return new AnikeenIdSsoUserProvider(
|
||||||
|
$app->make(AnikeenId::class),
|
||||||
|
$app->make(Request::class),
|
||||||
|
$config['model'],
|
||||||
|
$config['fields'] ?? [],
|
||||||
|
$config['access_token_field'] ?? null
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
reference the guard in the `guards` configuration of your `auth.php` configuration file:
|
||||||
|
|
||||||
|
```php
|
||||||
|
'guards' => [
|
||||||
|
'web' => [
|
||||||
|
'driver' => 'session',
|
||||||
|
'provider' => 'users',
|
||||||
|
],
|
||||||
|
|
||||||
|
'api' => [
|
||||||
|
'driver' => 'anikeen-id',
|
||||||
|
'provider' => 'sso-users',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
reference the provider in the `providers` configuration of your `auth.php` configuration file:
|
||||||
|
|
||||||
|
```php
|
||||||
|
'providers' => [
|
||||||
|
'users' => [
|
||||||
|
'driver' => 'eloquent',
|
||||||
|
'model' => App\Models\User::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'sso-users' => [
|
||||||
|
'driver' => 'sso-users',
|
||||||
|
'model' => App\Models\User::class,
|
||||||
|
'fields' => ['first_name', 'last_name', 'email'],
|
||||||
|
'access_token_field' => 'sso_access_token',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
#### Basic
|
||||||
|
|
||||||
|
```php
|
||||||
|
$anikeenId = new Anikeen\IdAnikeenId();
|
||||||
|
|
||||||
|
$anikeenId->setClientId('abc123');
|
||||||
|
|
||||||
|
// Get SSH Key by User ID
|
||||||
|
$result = $anikeenId->getSshKeysByUserId(38);
|
||||||
|
|
||||||
|
// Check, if the query was successfull
|
||||||
|
if ( ! $result->success()) {
|
||||||
|
die('Ooops: ' . $result->error());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift result to get single key data
|
||||||
|
$sshKey = $result->shift();
|
||||||
|
|
||||||
|
echo $sshKey->name;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Setters
|
||||||
|
|
||||||
|
```php
|
||||||
|
$anikeenId = new Anikeen\Id\AnikeenId();
|
||||||
|
|
||||||
|
$anikeenId->setClientId('abc123');
|
||||||
|
$anikeenId->setClientSecret('abc456');
|
||||||
|
$anikeenId->setToken('abcdef123456');
|
||||||
|
|
||||||
|
$anikeenId = $anikeenId->withClientId('abc123');
|
||||||
|
$anikeenId = $anikeenId->withClientSecret('abc123');
|
||||||
|
$anikeenId = $anikeenId->withToken('abcdef123456');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### OAuth Tokens
|
||||||
|
|
||||||
|
```php
|
||||||
|
$anikeenId = new Anikeen\Id\AnikeenId();
|
||||||
|
|
||||||
|
$anikeenId->setClientId('abc123');
|
||||||
|
$anikeenId->setToken('abcdef123456');
|
||||||
|
|
||||||
|
$result = $anikeenId->getAuthedUser();
|
||||||
|
|
||||||
|
$user = $userResult->shift();
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
$anikeenId->setToken('uvwxyz456789');
|
||||||
|
|
||||||
|
$result = $anikeenId->getAuthedUser();
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
$result = $anikeenId->withToken('uvwxyz456789')->getAuthedUser();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Facade
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Anikeen\Id\Facades\AnikeenId;
|
||||||
|
|
||||||
|
AnikeenId::withClientId('abc123')->withToken('abcdef123456')->getAuthedUser();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
### Oauth
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function retrievingToken(string $grantType, array $attributes)
|
||||||
|
```
|
||||||
|
|
||||||
|
### SshKeys
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function getSshKeysByUserId(int $id)
|
||||||
|
public function createSshKey(string $publicKey, string $name = NULL)
|
||||||
|
public function deleteSshKey(int $id)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Users
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function getAuthedUser()
|
||||||
|
public function createUser(array $parameters)
|
||||||
|
public function isEmailExisting(string $email)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete
|
||||||
|
|
||||||
|
```php
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get
|
||||||
|
|
||||||
|
```php
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Post
|
||||||
|
|
||||||
|
```php
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Put
|
||||||
|
|
||||||
|
```php
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
[**OAuth Scopes Enums**](https://github.com/anikeen-com/id/blob/main/src/Enums/Scope.php)
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
#### Run Tests
|
||||||
|
|
||||||
|
```shell
|
||||||
|
composer test
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
BASE_URL=xxxx CLIENT_ID=xxxx CLIENT_KEY=yyyy CLIENT_ACCESS_TOKEN=zzzz composer test
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Generate Documentation
|
||||||
|
|
||||||
|
```shell
|
||||||
|
composer docs
|
||||||
|
```
|
||||||
214
README.stub
Normal file
214
README.stub
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# Anikeen ID
|
||||||
|
|
||||||
|
[](https://packagist.org/packages/anikeen/id)
|
||||||
|
[](https://packagist.org/packages/anikeen/id)
|
||||||
|
[](https://packagist.org/packages/anikeen/id)
|
||||||
|
|
||||||
|
PHP Anikeen ID API Client for Laravel 11+
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
1. [Installation](#installation)
|
||||||
|
2. [Event Listener](#event-listener)
|
||||||
|
3. [Configuration](#configuration)
|
||||||
|
4. [Examples](#examples)
|
||||||
|
5. [Documentation](#documentation)
|
||||||
|
6. [Development](#Development)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
composer require anikeen/id
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Listener
|
||||||
|
|
||||||
|
In Laravel 11, the default EventServiceProvider provider was removed. Instead, add the listener using the listen method on the Event facade, in your `AppServiceProvider`
|
||||||
|
|
||||||
|
```
|
||||||
|
Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
|
||||||
|
$event->extendSocialite('anikeen-id', \Anikeen\Id\Socialite\Provider::class);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Copy configuration to config folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ php artisan vendor:publish --provider="Anikeen\Id\Providers\AnikeenIdServiceProvider"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add environmental variables to your `.env`
|
||||||
|
|
||||||
|
```
|
||||||
|
ANIKEEN_ID_KEY=
|
||||||
|
ANIKEEN_ID_SECRET=
|
||||||
|
ANIKEEN_ID_REDIRECT_URI=http://localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
You will need to add an entry to the services configuration file so that after config files are cached for usage in production environment (Laravel command `artisan config:cache`) all config is still available.
|
||||||
|
|
||||||
|
**Add to `config/services.php`:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
'anikeen-id' => [
|
||||||
|
'client_id' => env('ANIKEEN_ID_KEY'),
|
||||||
|
'client_secret' => env('ANIKEEN_ID_SECRET'),
|
||||||
|
'redirect' => env('ANIKEEN_ID_REDIRECT_URI')
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementing Auth
|
||||||
|
|
||||||
|
This method should typically be called in the `boot` method of your `AuthServiceProvider` class:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Anikeen\Id\AnikeenId;
|
||||||
|
use Anikeen\Id\Providers\AnikeenIdSsoUserProvider;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register any authentication / authorization services.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function boot()
|
||||||
|
{
|
||||||
|
Auth::provider('sso-users', function ($app, array $config) {
|
||||||
|
return new AnikeenIdSsoUserProvider(
|
||||||
|
$app->make(AnikeenId::class),
|
||||||
|
$app->make(Request::class),
|
||||||
|
$config['model'],
|
||||||
|
$config['fields'] ?? [],
|
||||||
|
$config['access_token_field'] ?? null
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
reference the guard in the `guards` configuration of your `auth.php` configuration file:
|
||||||
|
|
||||||
|
```php
|
||||||
|
'guards' => [
|
||||||
|
'web' => [
|
||||||
|
'driver' => 'session',
|
||||||
|
'provider' => 'users',
|
||||||
|
],
|
||||||
|
|
||||||
|
'api' => [
|
||||||
|
'driver' => 'anikeen-id',
|
||||||
|
'provider' => 'sso-users',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
reference the provider in the `providers` configuration of your `auth.php` configuration file:
|
||||||
|
|
||||||
|
```php
|
||||||
|
'providers' => [
|
||||||
|
'users' => [
|
||||||
|
'driver' => 'eloquent',
|
||||||
|
'model' => App\Models\User::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'sso-users' => [
|
||||||
|
'driver' => 'sso-users',
|
||||||
|
'model' => App\Models\User::class,
|
||||||
|
'fields' => ['first_name', 'last_name', 'email'],
|
||||||
|
'access_token_field' => 'sso_access_token',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
#### Basic
|
||||||
|
|
||||||
|
```php
|
||||||
|
$anikeenId = new Anikeen\IdAnikeenId();
|
||||||
|
|
||||||
|
$anikeenId->setClientId('abc123');
|
||||||
|
|
||||||
|
// Get SSH Key by User ID
|
||||||
|
$result = $anikeenId->getSshKeysByUserId(38);
|
||||||
|
|
||||||
|
// Check, if the query was successfull
|
||||||
|
if ( ! $result->success()) {
|
||||||
|
die('Ooops: ' . $result->error());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift result to get single key data
|
||||||
|
$sshKey = $result->shift();
|
||||||
|
|
||||||
|
echo $sshKey->name;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Setters
|
||||||
|
|
||||||
|
```php
|
||||||
|
$anikeenId = new Anikeen\Id\AnikeenId();
|
||||||
|
|
||||||
|
$anikeenId->setClientId('abc123');
|
||||||
|
$anikeenId->setClientSecret('abc456');
|
||||||
|
$anikeenId->setToken('abcdef123456');
|
||||||
|
|
||||||
|
$anikeenId = $anikeenId->withClientId('abc123');
|
||||||
|
$anikeenId = $anikeenId->withClientSecret('abc123');
|
||||||
|
$anikeenId = $anikeenId->withToken('abcdef123456');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### OAuth Tokens
|
||||||
|
|
||||||
|
```php
|
||||||
|
$anikeenId = new Anikeen\Id\AnikeenId();
|
||||||
|
|
||||||
|
$anikeenId->setClientId('abc123');
|
||||||
|
$anikeenId->setToken('abcdef123456');
|
||||||
|
|
||||||
|
$result = $anikeenId->getAuthedUser();
|
||||||
|
|
||||||
|
$user = $userResult->shift();
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
$anikeenId->setToken('uvwxyz456789');
|
||||||
|
|
||||||
|
$result = $anikeenId->getAuthedUser();
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
$result = $anikeenId->withToken('uvwxyz456789')->getAuthedUser();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Facade
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Anikeen\Id\Facades\AnikeenId;
|
||||||
|
|
||||||
|
AnikeenId::withClientId('abc123')->withToken('abcdef123456')->getAuthedUser();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
<!-- GENERATED-DOCS -->
|
||||||
|
|
||||||
|
[**OAuth Scopes Enums**](https://github.com/anikeen-com/id/blob/main/src/Enums/Scope.php)
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
#### Run Tests
|
||||||
|
|
||||||
|
```shell
|
||||||
|
composer test
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
BASE_URL=xxxx CLIENT_ID=xxxx CLIENT_KEY=yyyy CLIENT_ACCESS_TOKEN=zzzz composer test
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Generate Documentation
|
||||||
|
|
||||||
|
```shell
|
||||||
|
composer docs
|
||||||
|
```
|
||||||
52
composer.json
Normal file
52
composer.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"name": "anikeen/id",
|
||||||
|
"description": "PHP AnikeenId API Client for Laravel 10+",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "René Preuß",
|
||||||
|
"email": "rene@anikeen.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Maurice Preuß",
|
||||||
|
"email": "maurice@anikeen.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^8.0",
|
||||||
|
"ext-json": "*",
|
||||||
|
"illuminate/support": "^11.0|^12.0",
|
||||||
|
"illuminate/console": "^11.0|^12.0",
|
||||||
|
"guzzlehttp/guzzle": "^6.3|^7.0",
|
||||||
|
"socialiteproviders/manager": "^3.4|^4.0.1",
|
||||||
|
"firebase/php-jwt": "^6.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^8.0|^9.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Anikeen\\Id\\": "src/Id",
|
||||||
|
"Anikeen\\Support\\": "src/Support"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Anikeen\\Id\\Tests\\": "tests/Id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "vendor/bin/phpunit",
|
||||||
|
"docs": "php generator/generate-docs.php"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Anikeen\\Id\\Providers\\AnikeenIdServiceProvider"
|
||||||
|
],
|
||||||
|
"aliases": {
|
||||||
|
"AnikeenId": "Anikeen\\Id\\Facades\\AnikeenId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
config/anikeen-id.php
Normal file
8
config/anikeen-id.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'client_key' => env('ANIKEEN_ID_KEY'),
|
||||||
|
'client_secret' => env('ANIKEEN_ID_SECRET'),
|
||||||
|
'redirect_url' => env('ANIKEEN_ID_REDIRECT_URI'),
|
||||||
|
'base_url' => env('ANIKEEN_ID_BASE_URL'),
|
||||||
|
];
|
||||||
85
generator/generate-docs.php
Normal file
85
generator/generate-docs.php
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use Anikeen\Id\AnikeenId;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
$markdown = collect(class_uses(AnikeenId::class))
|
||||||
|
->map(function ($trait) {
|
||||||
|
|
||||||
|
$title = str_replace('Trait', '', Arr::last(explode('\\', $trait)));
|
||||||
|
|
||||||
|
$methods = [];
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($trait);
|
||||||
|
|
||||||
|
collect($reflection->getMethods())
|
||||||
|
->reject(function (ReflectionMethod $method) {
|
||||||
|
return $method->isAbstract();
|
||||||
|
})
|
||||||
|
->reject(function (ReflectionMethod $method) {
|
||||||
|
return $method->isPrivate() || $method->isProtected();
|
||||||
|
})
|
||||||
|
->reject(function (ReflectionMethod $method) {
|
||||||
|
return $method->isConstructor();
|
||||||
|
})
|
||||||
|
->each(function (ReflectionMethod $method) use (&$methods, $title, $trait) {
|
||||||
|
|
||||||
|
$declaration = collect($method->getModifiers())->map(function (int $modifier) {
|
||||||
|
return $modifier == ReflectionMethod::IS_PUBLIC ? 'public ' : '';
|
||||||
|
})->join(' ');
|
||||||
|
|
||||||
|
$declaration .= 'function ';
|
||||||
|
$declaration .= $method->getName();
|
||||||
|
$declaration .= '(';
|
||||||
|
|
||||||
|
$declaration .= collect($method->getParameters())->map(function (ReflectionParameter $parameter) {
|
||||||
|
|
||||||
|
$parameterString = Arr::last(explode('\\', $parameter->getType()->getName()));
|
||||||
|
$parameterString .= ' ';
|
||||||
|
$parameterString .= '$';
|
||||||
|
$parameterString .= $parameter->getName();
|
||||||
|
|
||||||
|
if ($parameter->isDefaultValueAvailable()) {
|
||||||
|
$parameterString .= ' = ';
|
||||||
|
$parameterString .= str_replace(PHP_EOL, '', var_export($parameter->getDefaultValue(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $parameterString;
|
||||||
|
|
||||||
|
})->join(', ');
|
||||||
|
|
||||||
|
$declaration .= ')';
|
||||||
|
|
||||||
|
$methods[] = $declaration;
|
||||||
|
});
|
||||||
|
|
||||||
|
return [$title, $methods];
|
||||||
|
})
|
||||||
|
->map(function ($args) {
|
||||||
|
|
||||||
|
list($title, $methods) = $args;
|
||||||
|
|
||||||
|
$markdown = '### ' . $title;
|
||||||
|
$markdown .= PHP_EOL . PHP_EOL;
|
||||||
|
$markdown .= '```php';
|
||||||
|
$markdown .= PHP_EOL;
|
||||||
|
|
||||||
|
$markdown .= collect($methods)->each(function ($method) {
|
||||||
|
return $method;
|
||||||
|
})->implode(PHP_EOL);
|
||||||
|
|
||||||
|
$markdown .= PHP_EOL;
|
||||||
|
$markdown .= '```';
|
||||||
|
|
||||||
|
return $markdown;
|
||||||
|
})->join(PHP_EOL . PHP_EOL);
|
||||||
|
|
||||||
|
$markdown = str_replace("array (\n)", '[]', $markdown);
|
||||||
|
|
||||||
|
$content = file_get_contents(__DIR__ . '/../README.stub');
|
||||||
|
|
||||||
|
$content = str_replace('<!-- GENERATED-DOCS -->', $markdown, $content);
|
||||||
|
|
||||||
|
file_put_contents(__DIR__ . '/../README.md', $content);
|
||||||
14
oauth-public.key
Normal file
14
oauth-public.key
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtpfqwoXAEOYbKIU0eBso
|
||||||
|
J0Amh40GsVKiZxPZ+GVftr96SQWI+wXGfFHAL9vNlucJqI0l2lsOEwI9UKr99w3M
|
||||||
|
borB9YmGhd15wX1Es5SYTEA6vo1qsLVz2gqDCW5AkmwI42WFWvBr9nR9+8qoNGgZ
|
||||||
|
neE2HLEq0BvtDBeebmWfurOJzAvRrXBj1qjYro50G5vFlTzga46P6I6y9/JevDFX
|
||||||
|
0IZVtPcUvvVfaOVu+oy2v5ET0k1KRXDq8ShR9oJbh9N/SOvzjS0tv7R3XfU6kIgF
|
||||||
|
SWw6nc9NOtpFfgscYLaVETNSF/HSW+UCYg0/aWKrcI0j/K+StfRArmeQE+rGysvt
|
||||||
|
4YPfH/4TztfqtirZKmCBSXnjyKpzWbrBE7ahxBqXhvF8vwegxFjfLs/aqAAXYgyN
|
||||||
|
X9zz91GobEdNBKct9whNl+OgGEIsfacEniPkQsRws/MDVhlrUyPu2R54sgrtboNf
|
||||||
|
wpuuz0hbBqjHjdPAAojRRdqp6EUXEn+8K0CTXqpogC30pGam4RsMgqP9/3kYjgQC
|
||||||
|
QQOoP/4hM6piKExf2JPQoc9AbVN9qbtKXZANfrEMbXjNd8a0yn65w68+lS959SkG
|
||||||
|
86NQxNCcmL07p/AtZMokZajuFsZEv1ezPt2IjCfUsjuB2+04EyCvBFv8q2GJQN39
|
||||||
|
suV26MnzxHOzo95+ViVwrAECAwEAAQ==
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
29
phpunit.xml
Normal file
29
phpunit.xml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit bootstrap="vendor/autoload.php"
|
||||||
|
backupGlobals="false"
|
||||||
|
backupStaticAttributes="false"
|
||||||
|
colors="true"
|
||||||
|
verbose="true"
|
||||||
|
convertErrorsToExceptions="true"
|
||||||
|
convertNoticesToExceptions="true"
|
||||||
|
convertWarningsToExceptions="true"
|
||||||
|
processIsolation="false"
|
||||||
|
stopOnFailure="false"
|
||||||
|
printerClass="\Codedungeon\PHPUnitPrettyResultPrinter\Printer">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit">
|
||||||
|
<directory>tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<filter>
|
||||||
|
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||||
|
<directory suffix=".php">src</directory>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
<php>
|
||||||
|
<env name="ANIKEEN_ID_KEY" value="38"/>
|
||||||
|
<env name="ANIKEEN_ID_SECRET" value="2goNRF8x37HPVZVaa28ySZGVXJuksvxnxM7TcGzM"/>
|
||||||
|
<env name="ANIKEEN_ID_BASE_URL" value="https://id.anikeen.com/api/v1/"/>
|
||||||
|
<env name="CLIENT_ACCESS_TOKEN" value="eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxNSIsImp0aSI6IjAxN2QxZDg0Y2MxNjAyZTYxOGZkNjYwZGViZWVjMDY4MTk2YmYzMDk1OGMzY2RiYzBjZmJkZWFjZjFhOTUxODQzZDU1YTk3OGY2YWIxY2YzIiwiaWF0IjoxNjY0NjIzMzA4LjQyODI4MSwibmJmIjoxNjY0NjIzMzA4LjQyODI4NCwiZXhwIjoxNjgwMzQ4MTA4LjQxNjc4MSwic3ViIjoiMzkiLCJzY29wZXMiOlsiYXBpIiwicmVhZF91c2VyIl0sImNsaWVudCI6eyJ0cnVzdGVkIjpmYWxzZX19.vxnzCaU4PpOrNVHa5AnGSS6gX_RCvxIERAnHFhjTrUzRafV9mr2Cvwd-BDVYoUr10wHvxa_TJSYfnAdDuhE-MEyDv13O3dL2XGTtJNa_Rg6L6CQ0JvC3wL-lWPvGPFax9pu-_lqbA3jm5B08hc3-7tq3f2nXcxjhtkqT6TTJv1-RCAppb2HCXiUDAqANzbhyInDjOH2WvFj1OGC_AI03J3W2KRWyeFLtUne8XKPFyr9XGcPuTrqogcuuXLeUt2kcf2bXbuIV1OlgIECrDiP1Ee0F2AzPs27ZVJ2z0R0JbT6AubKhGl5_Qi27cwjOH7hT2dmjcF1mLjzpN1uChLIdSnGSoStH8VzYHnHE2I8G-owW_aadG2UmGdnRY143q6g_28f3WIZNSucBSXkwFeS_t4fylsvpxhpjYJusf5qiEU_X3YbeawYMUCFUkSD2XTIypAqMJLNZQAeJ52eyL-9fln-Bv7n9v7K9G6ieR6Tm0tsJ1PRnaQi7rA1NTFwHoQmIOW9tfMycLzT7bgSoz3ra6Ez2J7ZNuWBZNKS0O-0YfSrAWyWK5U8YRfQuSVzP2VrIU63K6RGU2c284PfQGy11kgKUNQPykirb8p7MDQ8PwrxKaylBnD6hhDgjqEh2bfsr_43DfJA0R58L1HK3BmQnxgap0C77wK1e0yNlABpN28Q"/>
|
||||||
|
</php>
|
||||||
|
</phpunit>
|
||||||
371
src/Id/AnikeenId.php
Normal file
371
src/Id/AnikeenId.php
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id;
|
||||||
|
|
||||||
|
use Anikeen\Id\Exceptions\RequestRequiresAuthenticationException;
|
||||||
|
use Anikeen\Id\Exceptions\RequestRequiresClientIdException;
|
||||||
|
use Anikeen\Id\Exceptions\RequestRequiresRedirectUriException;
|
||||||
|
use Anikeen\Id\Helpers\Paginator;
|
||||||
|
use Anikeen\Support\Query;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
|
use GuzzleHttp\Exception\RequestException;
|
||||||
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
|
|
||||||
|
|
||||||
|
class AnikeenId
|
||||||
|
{
|
||||||
|
use Traits\OauthTrait;
|
||||||
|
use Traits\SshKeysTrait;
|
||||||
|
use Traits\UsersTrait;
|
||||||
|
|
||||||
|
use ApiOperations\Delete;
|
||||||
|
use ApiOperations\Get;
|
||||||
|
use ApiOperations\Post;
|
||||||
|
use ApiOperations\Put;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name for API token cookies.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public static string $cookie = 'anikeen_id_token';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if Anikeen ID should ignore incoming CSRF tokens.
|
||||||
|
*/
|
||||||
|
public static bool $ignoreCsrfToken = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if Anikeen ID should unserializes cookies.
|
||||||
|
*/
|
||||||
|
public static bool $unserializesCookies = false;
|
||||||
|
|
||||||
|
private static string $baseUrl = 'https://id.anikeen.com/api/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guzzle is used to make http requests.
|
||||||
|
*/
|
||||||
|
protected Client $client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paginator object.
|
||||||
|
*/
|
||||||
|
protected Paginator $paginator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anikeen ID OAuth token.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected ?string $token = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anikeen ID client id.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected ?string $clientId = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anikeen ID client secret.
|
||||||
|
*/
|
||||||
|
protected ?string $clientSecret = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anikeen ID OAuth redirect url.
|
||||||
|
*/
|
||||||
|
protected ?string $redirectUri = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
if ($clientId = config('anikeen_id.client_id')) {
|
||||||
|
$this->setClientId($clientId);
|
||||||
|
}
|
||||||
|
if ($clientSecret = config('anikeen_id.client_secret')) {
|
||||||
|
$this->setClientSecret($clientSecret);
|
||||||
|
}
|
||||||
|
if ($redirectUri = config('anikeen_id.redirect_url')) {
|
||||||
|
$this->setRedirectUri($redirectUri);
|
||||||
|
}
|
||||||
|
if ($redirectUri = config('anikeen_id.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(string $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.
|
||||||
|
*/
|
||||||
|
public static function actingAs(Authenticatable|Traits\HasAnikeenTokens $user, array $scopes = [], string $guard = 'api'): Authenticatable
|
||||||
|
{
|
||||||
|
$user->withAnikeenAccessToken((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.
|
||||||
|
*/
|
||||||
|
public function withClientId(string $clientId): self
|
||||||
|
{
|
||||||
|
$this->setClientId($clientId);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get client secret.
|
||||||
|
*
|
||||||
|
* @throws RequestRequiresClientIdException
|
||||||
|
*/
|
||||||
|
public function getClientSecret(): string
|
||||||
|
{
|
||||||
|
if (!$this->clientSecret) {
|
||||||
|
throw new RequestRequiresClientIdException;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set client secret.
|
||||||
|
*/
|
||||||
|
public function setClientSecret(string $clientSecret): void
|
||||||
|
{
|
||||||
|
$this->clientSecret = $clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fluid client secret setter.
|
||||||
|
*/
|
||||||
|
public function withClientSecret(string $clientSecret): self
|
||||||
|
{
|
||||||
|
$this->setClientSecret($clientSecret);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get redirect url.
|
||||||
|
*
|
||||||
|
* @throws RequestRequiresRedirectUriException
|
||||||
|
*/
|
||||||
|
public function getRedirectUri(): string
|
||||||
|
{
|
||||||
|
if (!$this->redirectUri) {
|
||||||
|
throw new RequestRequiresRedirectUriException;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set redirect url.
|
||||||
|
*/
|
||||||
|
public function setRedirectUri(string $redirectUri): void
|
||||||
|
{
|
||||||
|
$this->redirectUri = $redirectUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fluid redirect url setter.
|
||||||
|
*/
|
||||||
|
public function withRedirectUri(string $redirectUri): self
|
||||||
|
{
|
||||||
|
$this->setRedirectUri($redirectUri);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get OAuth token.
|
||||||
|
*
|
||||||
|
* @throws RequestRequiresAuthenticationException
|
||||||
|
*/
|
||||||
|
public function getToken(): ?string
|
||||||
|
{
|
||||||
|
if (!$this->token) {
|
||||||
|
throw new RequestRequiresAuthenticationException;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set OAuth token.
|
||||||
|
*/
|
||||||
|
public function setToken(string $token): void
|
||||||
|
{
|
||||||
|
$this->token = $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fluid OAuth token setter.
|
||||||
|
*/
|
||||||
|
public function withToken(string $token): self
|
||||||
|
{
|
||||||
|
$this->setToken($token);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* @throws GuzzleException
|
||||||
|
* @throws RequestRequiresClientIdException
|
||||||
|
*/
|
||||||
|
public function query(string $method = 'GET', string $path = '', array $parameters = [], Paginator $paginator = null, mixed $jsonBody = null): Result
|
||||||
|
{
|
||||||
|
/** @noinspection DuplicatedCode */
|
||||||
|
if ($paginator !== null) {
|
||||||
|
$parameters[$paginator->action] = $paginator->cursor();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$response = $this->client->request($method, $path, [
|
||||||
|
'headers' => $this->buildHeaders((bool)$jsonBody),
|
||||||
|
'query' => Query::build($parameters),
|
||||||
|
'json' => $jsonBody ?: null,
|
||||||
|
]);
|
||||||
|
$result = new Result($response, null, $paginator);
|
||||||
|
} catch (RequestException $exception) {
|
||||||
|
$result = new Result($exception->getResponse(), $exception, $paginator);
|
||||||
|
}
|
||||||
|
$result->anikeenId = $this;
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build headers for request.
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* @throws RequestRequiresClientIdException
|
||||||
|
*/
|
||||||
|
public function getClientId(): string
|
||||||
|
{
|
||||||
|
if (!$this->clientId) {
|
||||||
|
throw new RequestRequiresClientIdException;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set client id.
|
||||||
|
*/
|
||||||
|
public function setClientId(string $clientId): void
|
||||||
|
{
|
||||||
|
$this->clientId = $clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws GuzzleException
|
||||||
|
* @throws RequestRequiresClientIdException
|
||||||
|
*/
|
||||||
|
public function post(string $path = '', array $parameters = [], Paginator $paginator = null): Result
|
||||||
|
{
|
||||||
|
return $this->query('POST', $path, $parameters, $paginator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws GuzzleException
|
||||||
|
* @throws RequestRequiresClientIdException
|
||||||
|
*/
|
||||||
|
public function delete(string $path = '', array $parameters = [], Paginator $paginator = null): Result
|
||||||
|
{
|
||||||
|
return $this->query('DELETE', $path, $parameters, $paginator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws GuzzleException
|
||||||
|
* @throws RequestRequiresClientIdException
|
||||||
|
*/
|
||||||
|
public function put(string $path = '', array $parameters = [], Paginator $paginator = null): Result
|
||||||
|
{
|
||||||
|
return $this->query('PUT', $path, $parameters, $paginator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/Id/ApiOperations/Delete.php
Normal file
11
src/Id/ApiOperations/Delete.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\ApiOperations;
|
||||||
|
|
||||||
|
use Anikeen\Id\Helpers\Paginator;
|
||||||
|
use Anikeen\Id\Result;
|
||||||
|
|
||||||
|
trait Delete
|
||||||
|
{
|
||||||
|
abstract public function delete(string $path = '', array $parameters = [], Paginator $paginator = null): Result;
|
||||||
|
}
|
||||||
11
src/Id/ApiOperations/Get.php
Normal file
11
src/Id/ApiOperations/Get.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\ApiOperations;
|
||||||
|
|
||||||
|
use Anikeen\Id\Helpers\Paginator;
|
||||||
|
use Anikeen\Id\Result;
|
||||||
|
|
||||||
|
trait Get
|
||||||
|
{
|
||||||
|
abstract public function get(string $path = '', array $parameters = [], Paginator $paginator = null): Result;
|
||||||
|
}
|
||||||
11
src/Id/ApiOperations/Post.php
Normal file
11
src/Id/ApiOperations/Post.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\ApiOperations;
|
||||||
|
|
||||||
|
use Anikeen\Id\Helpers\Paginator;
|
||||||
|
use Anikeen\Id\Result;
|
||||||
|
|
||||||
|
trait Post
|
||||||
|
{
|
||||||
|
abstract public function post(string $path = '', array $parameters = [], Paginator $paginator = null): Result;
|
||||||
|
}
|
||||||
11
src/Id/ApiOperations/Put.php
Normal file
11
src/Id/ApiOperations/Put.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\ApiOperations;
|
||||||
|
|
||||||
|
use Anikeen\Id\Helpers\Paginator;
|
||||||
|
use Anikeen\Id\Result;
|
||||||
|
|
||||||
|
trait Put
|
||||||
|
{
|
||||||
|
abstract public function put(string $path = '', array $parameters = [], Paginator $paginator = null): Result;
|
||||||
|
}
|
||||||
19
src/Id/ApiOperations/Validation.php
Normal file
19
src/Id/ApiOperations/Validation.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\ApiOperations;
|
||||||
|
|
||||||
|
use Anikeen\Id\Exceptions\RequestRequiresMissingParametersException;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
trait Validation
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws RequestRequiresMissingParametersException
|
||||||
|
*/
|
||||||
|
public function validateRequired(array $parameters, array $required)
|
||||||
|
{
|
||||||
|
if (!Arr::has($parameters, $required)) {
|
||||||
|
throw RequestRequiresMissingParametersException::fromValidateRequired($parameters, $required);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/Id/ApiTokenCookieFactory.php
Normal file
54
src/Id/ApiTokenCookieFactory.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id;
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create an API token cookie factory instance.
|
||||||
|
*/
|
||||||
|
public function __construct(protected Config $config, protected Encrypter $encrypter)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new API token cookie.
|
||||||
|
*/
|
||||||
|
public function make(mixed $userId, string $csrfToken): Cookie
|
||||||
|
{
|
||||||
|
$config = $this->config->get('session');
|
||||||
|
|
||||||
|
$expiration = Carbon::now()->addMinutes($config['lifetime']);
|
||||||
|
|
||||||
|
return new Cookie(
|
||||||
|
AnikeenId::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.
|
||||||
|
*/
|
||||||
|
protected function createToken(mixed $userId, string $csrfToken, Carbon $expiration): string
|
||||||
|
{
|
||||||
|
return JWT::encode([
|
||||||
|
'sub' => $userId,
|
||||||
|
'csrf' => $csrfToken,
|
||||||
|
'expiry' => $expiration->getTimestamp(),
|
||||||
|
], $this->encrypter->getKey(), 'HS256');
|
||||||
|
}
|
||||||
|
}
|
||||||
199
src/Id/Auth/TokenGuard.php
Normal file
199
src/Id/Auth/TokenGuard.php
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Auth;
|
||||||
|
|
||||||
|
use Anikeen\Id\AnikeenId;
|
||||||
|
use Anikeen\Id\Helpers\JwtParser;
|
||||||
|
use Anikeen\Id\Traits\HasAnikeenTokens;
|
||||||
|
use Exception;
|
||||||
|
use Firebase\JWT\JWT;
|
||||||
|
use Firebase\JWT\Key;
|
||||||
|
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;
|
||||||
|
|
||||||
|
private Encrypter $encrypter;
|
||||||
|
|
||||||
|
private JwtParser $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.
|
||||||
|
*
|
||||||
|
* @throws BindingResolutionException
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
public function user(Request $request): ?Authenticatable
|
||||||
|
{
|
||||||
|
if ($request->bearerToken()) {
|
||||||
|
return $this->authenticateViaBearerToken($request);
|
||||||
|
} elseif ($request->cookie(AnikeenId::cookie())) {
|
||||||
|
return $this->authenticateViaCookie($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate the incoming request via the Bearer token.
|
||||||
|
*
|
||||||
|
* @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|HasAnikeenTokens $user */
|
||||||
|
$user = $this->provider->retrieveById(
|
||||||
|
$request->attributes->get('oauth_user_id') ?: null
|
||||||
|
);
|
||||||
|
|
||||||
|
return $user?->withAnikeenAccessToken($token);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate and get the incoming request via the Bearer token.
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
protected function authenticateViaCookie(Request $request): mixed
|
||||||
|
{
|
||||||
|
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|HasAnikeenTokens $user */
|
||||||
|
if ($user = $this->provider->retrieveById($token['sub'])) {
|
||||||
|
return $user->withAnikeenAccessToken((object)['scopes' => ['*']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the token cookie via the incoming request.
|
||||||
|
*/
|
||||||
|
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 (!AnikeenId::$ignoreCsrfToken && (!$this->validCsrf($token, $request) ||
|
||||||
|
time() >= $token['expiry'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode and decrypt the JWT token cookie.
|
||||||
|
*/
|
||||||
|
protected function decodeJwtTokenCookie(Request $request): array
|
||||||
|
{
|
||||||
|
return (array)JWT::decode(
|
||||||
|
CookieValuePrefix::remove($this->encrypter->decrypt($request->cookie(AnikeenId::cookie()), AnikeenId::$unserializesCookies)),
|
||||||
|
new Key(
|
||||||
|
$this->encrypter->getKey(),
|
||||||
|
'HS256'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the CSRF / header are valid and match.
|
||||||
|
*/
|
||||||
|
protected function validCsrf(array $token, Request $request): bool
|
||||||
|
{
|
||||||
|
return isset($token['csrf']) && hash_equals(
|
||||||
|
$token['csrf'], $this->getTokenFromRequest($request)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the CSRF token from the request.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
public static function serialized(): bool
|
||||||
|
{
|
||||||
|
return EncryptCookies::serialized('XSRF-TOKEN');
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/Id/Auth/UserProvider.php
Normal file
65
src/Id/Auth/UserProvider.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Auth;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
|
use Illuminate\Contracts\Auth\UserProvider as Base;
|
||||||
|
|
||||||
|
class UserProvider implements Base
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new AnikeenId user provider.
|
||||||
|
*/
|
||||||
|
public function __construct(protected Base $provider, protected string $providerName)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function retrieveById($identifier): ?Authenticatable
|
||||||
|
{
|
||||||
|
return $this->provider->retrieveById($identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function retrieveByToken($identifier, $token): ?Authenticatable
|
||||||
|
{
|
||||||
|
return $this->provider->retrieveByToken($identifier, $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function updateRememberToken(Authenticatable $user, $token): void
|
||||||
|
{
|
||||||
|
$this->provider->updateRememberToken($user, $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function retrieveByCredentials(array $credentials): ?Authenticatable
|
||||||
|
{
|
||||||
|
return $this->provider->retrieveByCredentials($credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function validateCredentials(Authenticatable $user, array $credentials): bool
|
||||||
|
{
|
||||||
|
return $this->provider->validateCredentials($user, $credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the user provider.
|
||||||
|
*/
|
||||||
|
public function getProviderName(): string
|
||||||
|
{
|
||||||
|
return $this->providerName;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/Id/Contracts/AppTokenRepository.php
Normal file
13
src/Id/Contracts/AppTokenRepository.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Contracts;
|
||||||
|
|
||||||
|
use Anikeen\Id\Exceptions\RequestFreshAccessTokenException;
|
||||||
|
|
||||||
|
interface AppTokenRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws RequestFreshAccessTokenException
|
||||||
|
*/
|
||||||
|
public function getAccessToken(): string;
|
||||||
|
}
|
||||||
16
src/Id/Enums/Scope.php
Normal file
16
src/Id/Enums/Scope.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Enums;
|
||||||
|
|
||||||
|
class Scope
|
||||||
|
{
|
||||||
|
const USER = 'user';
|
||||||
|
const USER_READ = 'user:read';
|
||||||
|
const ORDERS = 'orders';
|
||||||
|
const ORDERS_READ = 'orders:read';
|
||||||
|
const PRODUCTS = 'products';
|
||||||
|
const PRODUCTS_READ = 'products:read';
|
||||||
|
const BILLING = 'billing';
|
||||||
|
const BILLING_READ = 'billing:read';
|
||||||
|
const ADMIN = 'admin';
|
||||||
|
}
|
||||||
32
src/Id/Exceptions/MissingScopeException.php
Normal file
32
src/Id/Exceptions/MissingScopeException.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Exceptions;
|
||||||
|
|
||||||
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
class MissingScopeException extends AuthorizationException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The scopes that the user did not have.
|
||||||
|
*/
|
||||||
|
protected array $scopes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new missing scope exception.
|
||||||
|
*/
|
||||||
|
public function __construct(array|string $scopes = [], $message = 'Invalid scope(s) provided.')
|
||||||
|
{
|
||||||
|
parent::__construct($message);
|
||||||
|
|
||||||
|
$this->scopes = Arr::wrap($scopes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the scopes that the user did not have.
|
||||||
|
*/
|
||||||
|
public function scopes(): array
|
||||||
|
{
|
||||||
|
return $this->scopes;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/Id/Exceptions/RateLimitException.php
Normal file
14
src/Id/Exceptions/RateLimitException.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class RateLimitException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct($message = 'Rate Limit exceeded', $code = 0, Exception $previous = null)
|
||||||
|
{
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Id/Exceptions/RequestFreshAccessTokenException.php
Normal file
24
src/Id/Exceptions/RequestFreshAccessTokenException.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Exceptions;
|
||||||
|
|
||||||
|
use DomainException;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
|
class RequestFreshAccessTokenException extends DomainException
|
||||||
|
{
|
||||||
|
private ResponseInterface $response;
|
||||||
|
|
||||||
|
public static function fromResponse(ResponseInterface $response): self
|
||||||
|
{
|
||||||
|
$instance = new self(sprintf('Refresh token request from AnikeenId failed. Status Code is %s.', $response->getStatusCode()));
|
||||||
|
$instance->response = $response;
|
||||||
|
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResponse(): ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->response;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/Id/Exceptions/RequestRequiresAuthenticationException.php
Normal file
13
src/Id/Exceptions/RequestRequiresAuthenticationException.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class RequestRequiresAuthenticationException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct($message = 'Request requires authentication', $code = 0, Exception $previous = null)
|
||||||
|
{
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/Id/Exceptions/RequestRequiresClientIdException.php
Normal file
13
src/Id/Exceptions/RequestRequiresClientIdException.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class RequestRequiresClientIdException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct($message = 'Request requires Client-ID', $code = 0, Exception $previous = null)
|
||||||
|
{
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class RequestRequiresMissingParametersException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct($message = 'Request requires missing parameters', $code = 0, Exception $previous = null)
|
||||||
|
{
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromValidateRequired(array $given, array $required): RequestRequiresMissingParametersException
|
||||||
|
{
|
||||||
|
return new self(sprintf(
|
||||||
|
'Request requires missing parameters. Required: %s. Given: %s',
|
||||||
|
implode(', ', $required),
|
||||||
|
implode(', ', $given)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/Id/Exceptions/RequestRequiresParameter.php
Normal file
13
src/Id/Exceptions/RequestRequiresParameter.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class RequestRequiresParameter extends Exception
|
||||||
|
{
|
||||||
|
public function __construct($message = 'Request requires parameters', $code = 0, Exception $previous = null)
|
||||||
|
{
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/Id/Exceptions/RequestRequiresRedirectUriException.php
Normal file
13
src/Id/Exceptions/RequestRequiresRedirectUriException.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class RequestRequiresRedirectUriException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct($message = 'Request requires redirect uri', $code = 0, Exception $previous = null)
|
||||||
|
{
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/Id/Facades/AnikeenId.php
Normal file
21
src/Id/Facades/AnikeenId.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Facades;
|
||||||
|
|
||||||
|
use Anikeen\Id\AnikeenId as AnikeenIdService;
|
||||||
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @method static string|static cookie(string $cookie = null)
|
||||||
|
* @method static Authenticatable actingAs($user, $scopes = [], $guard = 'api')
|
||||||
|
* @method static static withClientId(string $clientId): self
|
||||||
|
* @method static string getClientSecret(): string
|
||||||
|
*/
|
||||||
|
class AnikeenId extends Facade
|
||||||
|
{
|
||||||
|
protected static function getFacadeAccessor(): string
|
||||||
|
{
|
||||||
|
return AnikeenIdService::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/Id/Helpers/JwtParser.php
Normal file
35
src/Id/Helpers/JwtParser.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Helpers;
|
||||||
|
|
||||||
|
use Firebase\JWT\JWT;
|
||||||
|
use Firebase\JWT\Key;
|
||||||
|
use Illuminate\Auth\AuthenticationException;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use stdClass;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class JwtParser
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws AuthenticationException
|
||||||
|
*/
|
||||||
|
public function decode(Request $request): stdClass
|
||||||
|
{
|
||||||
|
JWT::$leeway = 60;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JWT::decode(
|
||||||
|
$request->bearerToken(),
|
||||||
|
new Key($this->getOauthPublicKey(), 'RS256')
|
||||||
|
);
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
throw (new AuthenticationException());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getOauthPublicKey(): bool|string
|
||||||
|
{
|
||||||
|
return file_get_contents(dirname(__DIR__, 3) . '/oauth-public.key');
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/Id/Helpers/Paginator.php
Normal file
76
src/Id/Helpers/Paginator.php
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Helpers;
|
||||||
|
|
||||||
|
use Anikeen\Id\Result;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
class Paginator
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Next desired action (first, after, before).
|
||||||
|
*/
|
||||||
|
public ?string $action = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AnikeenId response pagination cursor.
|
||||||
|
*/
|
||||||
|
private ?stdClass $pagination;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param null|stdClass $pagination AnikeenId response pagination cursor
|
||||||
|
*/
|
||||||
|
public function __construct(?stdClass $pagination = null)
|
||||||
|
{
|
||||||
|
$this->pagination = $pagination;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Paginator from Result object.
|
||||||
|
*/
|
||||||
|
public static function from(Result $result): self
|
||||||
|
{
|
||||||
|
return new self($result->pagination);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current active cursor.
|
||||||
|
*/
|
||||||
|
public function cursor(): string
|
||||||
|
{
|
||||||
|
return $this->pagination->cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the Paginator to fetch the next set of results.
|
||||||
|
*/
|
||||||
|
public function first(): self
|
||||||
|
{
|
||||||
|
$this->action = 'first';
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the Paginator to fetch the first set of results.
|
||||||
|
*/
|
||||||
|
public function next(): self
|
||||||
|
{
|
||||||
|
$this->action = 'after';
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the Paginator to fetch the last set of results.
|
||||||
|
*/
|
||||||
|
public function back(): self
|
||||||
|
{
|
||||||
|
$this->action = 'before';
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/Id/Http/Middleware/CheckClientCredentials.php
Normal file
27
src/Id/Http/Middleware/CheckClientCredentials.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Http\Middleware;
|
||||||
|
|
||||||
|
use Anikeen\Id\Exceptions\MissingScopeException;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
class CheckClientCredentials extends CheckCredentials
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validate token credentials.
|
||||||
|
*
|
||||||
|
* @throws MissingScopeException
|
||||||
|
*/
|
||||||
|
protected function validateScopes(stdClass $token, array $scopes): void
|
||||||
|
{
|
||||||
|
if (in_array('*', $token->scopes)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($scopes as $scope) {
|
||||||
|
if (!in_array($scope, $token->scopes)) {
|
||||||
|
throw new MissingScopeException($scopes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/Id/Http/Middleware/CheckClientCredentialsForAnyScope.php
Normal file
29
src/Id/Http/Middleware/CheckClientCredentialsForAnyScope.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Http\Middleware;
|
||||||
|
|
||||||
|
use Anikeen\Id\Exceptions\MissingScopeException;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
class CheckClientCredentialsForAnyScope extends CheckCredentials
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validate token credentials.
|
||||||
|
*
|
||||||
|
* @throws MissingScopeException
|
||||||
|
*/
|
||||||
|
protected function validateScopes(stdClass $token, array $scopes): void
|
||||||
|
{
|
||||||
|
if (in_array('*', $token->scopes)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($scopes as $scope) {
|
||||||
|
if (in_array($scope, $token->scopes)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new MissingScopeException($scopes);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/Id/Http/Middleware/CheckCredentials.php
Normal file
40
src/Id/Http/Middleware/CheckCredentials.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Http\Middleware;
|
||||||
|
|
||||||
|
use Anikeen\Id\Exceptions\MissingScopeException;
|
||||||
|
use Anikeen\Id\Helpers\JwtParser;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Auth\AuthenticationException;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
abstract class CheckCredentials
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @throws AuthenticationException|MissingScopeException
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next, ...$scopes): mixed
|
||||||
|
{
|
||||||
|
$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);
|
||||||
|
}
|
||||||
32
src/Id/Http/Middleware/CheckForAnyScope.php
Normal file
32
src/Id/Http/Middleware/CheckForAnyScope.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Http\Middleware;
|
||||||
|
|
||||||
|
use Anikeen\Id\Exceptions\MissingScopeException;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Auth\AuthenticationException;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
|
||||||
|
class CheckForAnyScope
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the incoming request.
|
||||||
|
*
|
||||||
|
* @throws AuthenticationException|MissingScopeException
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next, ...$scopes): Response
|
||||||
|
{
|
||||||
|
if (!$request->user() || !$request->user()->anikeenToken()) {
|
||||||
|
throw new AuthenticationException;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($scopes as $scope) {
|
||||||
|
if ($request->user()->anikeenTokenCan($scope)) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new MissingScopeException($scopes);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/Id/Http/Middleware/CheckScopes.php
Normal file
32
src/Id/Http/Middleware/CheckScopes.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Http\Middleware;
|
||||||
|
|
||||||
|
use Anikeen\Id\Exceptions\MissingScopeException;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Auth\AuthenticationException;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
|
||||||
|
class CheckScopes
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the incoming request.
|
||||||
|
*
|
||||||
|
* @throws AuthenticationException|MissingScopeException
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next, ...$scopes): Response
|
||||||
|
{
|
||||||
|
if (!$request->user() || !$request->user()->anikeenToken()) {
|
||||||
|
throw new AuthenticationException;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($scopes as $scope) {
|
||||||
|
if (!$request->user()->anikeenTokenCan($scope)) {
|
||||||
|
throw new MissingScopeException($scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/Id/Http/Middleware/CreateFreshApiToken.php
Normal file
87
src/Id/Http/Middleware/CreateFreshApiToken.php
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Http\Middleware;
|
||||||
|
|
||||||
|
use Anikeen\Id\ApiTokenCookieFactory;
|
||||||
|
use Anikeen\Id\Facades\AnikeenId;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
|
||||||
|
class CreateFreshApiToken
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The authentication guard.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected string $guard;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new middleware instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(protected ApiTokenCookieFactory $cookieFactory)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next, string $guard = null): mixed
|
||||||
|
{
|
||||||
|
$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.
|
||||||
|
*/
|
||||||
|
protected function shouldReceiveFreshToken(Request $request, Response $response): bool
|
||||||
|
{
|
||||||
|
return $this->requestShouldReceiveFreshToken($request) &&
|
||||||
|
$this->responseShouldReceiveFreshToken($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the request should receive a fresh token.
|
||||||
|
*/
|
||||||
|
protected function requestShouldReceiveFreshToken(Request $request): bool
|
||||||
|
{
|
||||||
|
return $request->isMethod('GET') && $request->user($this->guard);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the response should receive a fresh token.
|
||||||
|
*/
|
||||||
|
protected function responseShouldReceiveFreshToken(Response $response): bool
|
||||||
|
{
|
||||||
|
return !$this->alreadyContainsToken($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the given response already contains an API token.
|
||||||
|
* This avoids us overwriting a just "refreshed" token.
|
||||||
|
*/
|
||||||
|
protected function alreadyContainsToken(Response $response): bool
|
||||||
|
{
|
||||||
|
foreach ($response->headers->getCookies() as $cookie) {
|
||||||
|
if ($cookie->getName() === AnikeenId::cookie()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/Id/Providers/AnikeenIdServiceProvider.php
Normal file
80
src/Id/Providers/AnikeenIdServiceProvider.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Providers;
|
||||||
|
|
||||||
|
use Anikeen\Id\AnikeenId;
|
||||||
|
use Anikeen\Id\Auth\TokenGuard;
|
||||||
|
use Anikeen\Id\Auth\UserProvider;
|
||||||
|
use Anikeen\Id\Contracts;
|
||||||
|
use Anikeen\Id\Helpers\JwtParser;
|
||||||
|
use Anikeen\Id\Repository;
|
||||||
|
use Illuminate\Auth\RequestGuard;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class AnikeenIdServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Bootstrap the application services.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function boot()
|
||||||
|
{
|
||||||
|
$this->publishes([
|
||||||
|
dirname(__DIR__, 3) . '/config/anikeen-id.php' => config_path('anikeen-id.php'),
|
||||||
|
], 'config');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
$this->mergeConfigFrom(dirname(__DIR__, 3) . '/config/anikeen-id.php', 'anikeen-id');
|
||||||
|
$this->app->singleton(Contracts\AppTokenRepository::class, Repository\AppTokenRepository::class);
|
||||||
|
$this->app->singleton(AnikeenId::class, function () {
|
||||||
|
return new AnikeenId;
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->registerGuard();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the token guard.
|
||||||
|
*/
|
||||||
|
protected function registerGuard(): void
|
||||||
|
{
|
||||||
|
Auth::resolved(function ($auth) {
|
||||||
|
$auth->extend('anikeen-id', 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.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
public function provides(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
AnikeenId::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/Id/Providers/AnikeenIdSsoUserProvider.php
Normal file
117
src/Id/Providers/AnikeenIdSsoUserProvider.php
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Providers;
|
||||||
|
|
||||||
|
use Anikeen\Id\AnikeenId;
|
||||||
|
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 AnikeenIdSsoUserProvider implements UserProvider
|
||||||
|
{
|
||||||
|
private AnikeenId $anikeenId;
|
||||||
|
private ?string $accessTokenField = null;
|
||||||
|
private array $fields;
|
||||||
|
private string $model;
|
||||||
|
private Request $request;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
AnikeenId $anikeenId,
|
||||||
|
Request $request,
|
||||||
|
string $model,
|
||||||
|
array $fields,
|
||||||
|
?string $accessTokenField = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
$this->request = $request;
|
||||||
|
$this->model = $model;
|
||||||
|
$this->fields = $fields;
|
||||||
|
$this->accessTokenField = $accessTokenField;
|
||||||
|
$this->anikeenId = $anikeenId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function retrieveById(mixed $identifier): Builder|Model|null
|
||||||
|
{
|
||||||
|
$model = $this->createModel();
|
||||||
|
$token = $this->request->bearerToken();
|
||||||
|
|
||||||
|
$user = $this->newModelQuery($model)
|
||||||
|
->where($model->getAuthIdentifierName(), $identifier)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
// Return user when found
|
||||||
|
if ($user) {
|
||||||
|
// Update access token when updated
|
||||||
|
if ($this->accessTokenField) {
|
||||||
|
$user[$this->accessTokenField] = $token;
|
||||||
|
|
||||||
|
if ($user->isDirty()) {
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new user
|
||||||
|
$this->anikeenId->setToken($token);
|
||||||
|
$result = $this->anikeenId->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.
|
||||||
|
*/
|
||||||
|
public function createModel(): Model
|
||||||
|
{
|
||||||
|
$class = '\\' . ltrim($this->model, '\\');
|
||||||
|
|
||||||
|
return new $class;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a new query builder for the model instance.
|
||||||
|
*/
|
||||||
|
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): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/Id/Repository/AppTokenRepository.php
Normal file
57
src/Id/Repository/AppTokenRepository.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Repository;
|
||||||
|
|
||||||
|
use Anikeen\Id\AnikeenId;
|
||||||
|
use Anikeen\Id\Contracts\AppTokenRepository as Repository;
|
||||||
|
use Anikeen\Id\Exceptions\RequestFreshAccessTokenException;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class AppTokenRepository implements Repository
|
||||||
|
{
|
||||||
|
public const ACCESS_TOKEN_CACHE_KEY = 'anikeen-id:access_token';
|
||||||
|
|
||||||
|
private AnikeenId $client;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->client = app(AnikeenId::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getAccessToken(): string
|
||||||
|
{
|
||||||
|
$accessToken = Cache::get(self::ACCESS_TOKEN_CACHE_KEY);
|
||||||
|
|
||||||
|
if ($accessToken) {
|
||||||
|
return $accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->requestFreshAccessToken('*');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RequestFreshAccessTokenException
|
||||||
|
*/
|
||||||
|
private function requestFreshAccessToken(string $scope): mixed
|
||||||
|
{
|
||||||
|
$result = $this->getClient()->retrievingToken('client_credentials', [
|
||||||
|
'scope' => $scope,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$result->success()) {
|
||||||
|
throw RequestFreshAccessTokenException::fromResponse($result->response());
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache::put(self::ACCESS_TOKEN_CACHE_KEY, $accessToken = $result->data()->access_token, now()->addWeek());
|
||||||
|
|
||||||
|
return $accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getClient(): AnikeenId
|
||||||
|
{
|
||||||
|
return $this->client;
|
||||||
|
}
|
||||||
|
}
|
||||||
203
src/Id/Result.php
Normal file
203
src/Id/Result.php
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id;
|
||||||
|
|
||||||
|
use Anikeen\Id\Helpers\Paginator;
|
||||||
|
use Exception;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
class Result
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query successful.
|
||||||
|
*/
|
||||||
|
public bool $success = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query result data.
|
||||||
|
*/
|
||||||
|
public array $data = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total amount of result data.
|
||||||
|
*/
|
||||||
|
public int $total = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status Code.
|
||||||
|
*/
|
||||||
|
public int $status = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AnikeenId response pagination cursor.
|
||||||
|
*/
|
||||||
|
public ?stdClass $pagination;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Original AnikeenId instance.
|
||||||
|
*
|
||||||
|
* @var AnikeenId
|
||||||
|
*/
|
||||||
|
public AnikeenId $anikeenId;
|
||||||
|
|
||||||
|
public function __construct(public ?ResponseInterface $response, public ?Exception $exception = null, public ?Paginator $paginator = null)
|
||||||
|
{
|
||||||
|
$this->success = $exception === null;
|
||||||
|
$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.
|
||||||
|
*/
|
||||||
|
private function setProperty(stdClass $jsonResponse, string $responseProperty, string $attribute = null): void
|
||||||
|
{
|
||||||
|
$classAttribute = $attribute ?? $responseProperty;
|
||||||
|
if (property_exists($jsonResponse, $responseProperty)) {
|
||||||
|
$this->{$classAttribute} = $jsonResponse->{$responseProperty};
|
||||||
|
} elseif ($responseProperty === 'data') {
|
||||||
|
$this->{$classAttribute} = $jsonResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the query was successfully.
|
||||||
|
*/
|
||||||
|
public function success(): bool
|
||||||
|
{
|
||||||
|
return $this->success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the last HTTP or API error.
|
||||||
|
*/
|
||||||
|
public function error(): string
|
||||||
|
{
|
||||||
|
// TODO Switch Exception response parsing to this->data
|
||||||
|
if ($this->exception === null || !$this->exception->hasResponse()) {
|
||||||
|
return 'Anikeen ID 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).
|
||||||
|
*/
|
||||||
|
public function shift(): mixed
|
||||||
|
{
|
||||||
|
if (!empty($this->data)) {
|
||||||
|
$data = $this->data;
|
||||||
|
|
||||||
|
return array_shift($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current count of items in dataset.
|
||||||
|
*/
|
||||||
|
public function count(): int
|
||||||
|
{
|
||||||
|
return count($this->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the Paginator to fetch the next set of results.
|
||||||
|
*/
|
||||||
|
public function next(): ?Paginator
|
||||||
|
{
|
||||||
|
return $this->paginator?->next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the Paginator to fetch the last set of results.
|
||||||
|
*/
|
||||||
|
public function back(): ?Paginator
|
||||||
|
{
|
||||||
|
return $this->paginator?->back();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rate limit information.
|
||||||
|
*/
|
||||||
|
public function rateLimit(string $key = null): array|int|string|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.
|
||||||
|
*/
|
||||||
|
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->anikeenId->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.
|
||||||
|
*/
|
||||||
|
public function first(): ?Paginator
|
||||||
|
{
|
||||||
|
return $this->paginator?->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function response(): ?ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dump(): void
|
||||||
|
{
|
||||||
|
dump($this->data());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the response data, also available as public attribute.
|
||||||
|
*/
|
||||||
|
public function data(): array
|
||||||
|
{
|
||||||
|
return $this->data;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/Id/Socialite/AnikeenIdExtendSocialite.php
Normal file
13
src/Id/Socialite/AnikeenIdExtendSocialite.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Socialite;
|
||||||
|
|
||||||
|
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||||
|
|
||||||
|
class AnikeenIdExtendSocialite
|
||||||
|
{
|
||||||
|
public function handle(SocialiteWasCalled $socialiteWasCalled): void
|
||||||
|
{
|
||||||
|
$socialiteWasCalled->extendSocialite('anikeen-id', Provider::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/Id/Socialite/Provider.php
Normal file
88
src/Id/Socialite/Provider.php
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Socialite;
|
||||||
|
|
||||||
|
use Anikeen\Id\Enums\Scope;
|
||||||
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Laravel\Socialite\Two\ProviderInterface;
|
||||||
|
use SocialiteProviders\Manager\OAuth2\AbstractProvider;
|
||||||
|
use SocialiteProviders\Manager\OAuth2\User;
|
||||||
|
|
||||||
|
class Provider extends AbstractProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Unique Provider Identifier.
|
||||||
|
*/
|
||||||
|
const IDENTIFIER = 'ANIKEEN_ID';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected $scopes = [Scope::USER_READ];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inherticdoc}
|
||||||
|
*/
|
||||||
|
protected $scopeSeparator = ' ';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function getAuthUrl($state): string
|
||||||
|
{
|
||||||
|
return $this->buildAuthUrlFromBase(
|
||||||
|
'https://id.anikeen.com/oauth/authorize', $state
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function getTokenUrl(): string
|
||||||
|
{
|
||||||
|
return 'https://id.anikeen.com/oauth/token';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @throws GuzzleException
|
||||||
|
*/
|
||||||
|
protected function getUserByToken($token)
|
||||||
|
{
|
||||||
|
$response = $this->getHttpClient()->get(
|
||||||
|
'https://id.anikeen.com/api/v1/user', [
|
||||||
|
'headers' => [
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
'Authorization' => 'Bearer ' . $token,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return json_decode($response->getBody()->getContents(), true)['data'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function mapUserToObject(array $user): \Laravel\Socialite\Two\User|User
|
||||||
|
{
|
||||||
|
return (new User())->setRaw($user)->map([
|
||||||
|
'id' => $user['id'],
|
||||||
|
'nickname' => $user['username'],
|
||||||
|
'name' => $user['name'],
|
||||||
|
'email' => Arr::get($user, 'email'),
|
||||||
|
'avatar' => $user['avatar'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function getTokenFields($code): array
|
||||||
|
{
|
||||||
|
return array_merge(parent::getTokenFields($code), [
|
||||||
|
'grant_type' => 'authorization_code',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/Id/Traits/HasAnikeenTokens.php
Normal file
43
src/Id/Traits/HasAnikeenTokens.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Traits;
|
||||||
|
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
trait HasAnikeenTokens
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The current access token for the authentication user.
|
||||||
|
*/
|
||||||
|
protected ?stdClass $accessToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current access token being used by the user.
|
||||||
|
*
|
||||||
|
* @return stdClass|null
|
||||||
|
*/
|
||||||
|
public function anikeenToken(): ?stdClass
|
||||||
|
{
|
||||||
|
return $this->accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the current API token has a given scope.
|
||||||
|
*/
|
||||||
|
public function anikeenTokenCan(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.
|
||||||
|
*/
|
||||||
|
public function withAnikeenAccessToken(stdClass $accessToken): self
|
||||||
|
{
|
||||||
|
$this->accessToken = $accessToken;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/Id/Traits/OauthTrait.php
Normal file
36
src/Id/Traits/OauthTrait.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Traits;
|
||||||
|
|
||||||
|
use Anikeen\Id\Result;
|
||||||
|
use GuzzleHttp\Exception\RequestException;
|
||||||
|
|
||||||
|
|
||||||
|
trait OauthTrait
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieving an oauth token using a given grant type.
|
||||||
|
*/
|
||||||
|
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->anikeenId = $this;
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/Id/Traits/SshKeysTrait.php
Normal file
42
src/Id/Traits/SshKeysTrait.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Traits;
|
||||||
|
|
||||||
|
use Anikeen\Id\ApiOperations\Delete;
|
||||||
|
use Anikeen\Id\ApiOperations\Get;
|
||||||
|
use Anikeen\Id\ApiOperations\Post;
|
||||||
|
use Anikeen\Id\Result;
|
||||||
|
|
||||||
|
trait SshKeysTrait
|
||||||
|
{
|
||||||
|
|
||||||
|
use Get, Post, Delete;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get currently authed user with Bearer Token
|
||||||
|
*/
|
||||||
|
public function getSshKeysByUserId(int $id): Result
|
||||||
|
{
|
||||||
|
return $this->get("v1/users/$id/ssh-keys/json", [], null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates ssh key for the currently authed user
|
||||||
|
*/
|
||||||
|
public function createSshKey(string $publicKey, string $name = null): Result
|
||||||
|
{
|
||||||
|
return $this->post('v1/ssh-keys', [
|
||||||
|
'public_key' => $publicKey,
|
||||||
|
'name' => $name,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a given ssh key for the currently authed user
|
||||||
|
*/
|
||||||
|
public function deleteSshKey(int $id): Result
|
||||||
|
{
|
||||||
|
return $this->delete("v1/ssh-keys/$id", []);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/Id/Traits/UsersTrait.php
Normal file
39
src/Id/Traits/UsersTrait.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace Anikeen\Id\Traits;
|
||||||
|
|
||||||
|
use Anikeen\Id\ApiOperations\Get;
|
||||||
|
use Anikeen\Id\Result;
|
||||||
|
|
||||||
|
trait UsersTrait
|
||||||
|
{
|
||||||
|
|
||||||
|
use Get;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get currently authed user with Bearer Token
|
||||||
|
*/
|
||||||
|
public function getAuthedUser(): Result
|
||||||
|
{
|
||||||
|
return $this->get('v1/user');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new user on behalf of the current user.
|
||||||
|
*/
|
||||||
|
public function createUser(array $parameters): Result
|
||||||
|
{
|
||||||
|
return $this->post('v1/users', $parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given email exists.
|
||||||
|
*/
|
||||||
|
public function isEmailExisting(string $email): Result
|
||||||
|
{
|
||||||
|
return $this->post('v1/users/check', [
|
||||||
|
'email' => $email,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/Support/Query.php
Normal file
26
src/Support/Query.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Anikeen\Support;
|
||||||
|
|
||||||
|
class Query
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Build query with support for multiple same first-dimension keys.
|
||||||
|
*
|
||||||
|
* @param array $query
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function build(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user