Make authentication more reliable for list syncing

This commit is contained in:
Timothy Warren 2020-05-01 17:08:20 -04:00
parent 9108fe066a
commit a7e6b3f198
8 changed files with 216 additions and 120 deletions

View File

@ -21,7 +21,6 @@ use const Aviat\AnimeClient\USER_AGENT;
use function Amp\Promise\wait; use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse; use function Aviat\AnimeClient\getResponse;
use Amp;
use Amp\Http\Client\Request; use Amp\Http\Client\Request;
use Amp\Http\Client\Body\FormBody; use Amp\Http\Client\Body\FormBody;
use Aviat\Ion\Json; use Aviat\Ion\Json;

View File

@ -54,7 +54,7 @@ class AnimeListTransformer extends AbstractTransformer {
'reconsuming' => $reconsuming, 'reconsuming' => $reconsuming,
'status' => $reconsuming 'status' => $reconsuming
? KitsuStatus::WATCHING ? KitsuStatus::WATCHING
:AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']], : AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']],
'updatedAt' => (new DateTime()) 'updatedAt' => (new DateTime())
->setTimestamp($item['updatedAt']) ->setTimestamp($item['updatedAt'])
->format(DateTime::W3C) ->format(DateTime::W3C)

View File

@ -17,6 +17,7 @@
namespace Aviat\AnimeClient\API\Anilist\Transformer; namespace Aviat\AnimeClient\API\Anilist\Transformer;
use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Anilist as AnilistStatus; use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Anilist as AnilistStatus;
use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Kitsu as KitsuStatus;
use Aviat\AnimeClient\API\Mapping\MangaReadingStatus; use Aviat\AnimeClient\API\Mapping\MangaReadingStatus;
use Aviat\AnimeClient\Types\MangaListItem; use Aviat\AnimeClient\Types\MangaListItem;
use Aviat\AnimeClient\Types\FormItem; use Aviat\AnimeClient\Types\FormItem;
@ -40,6 +41,8 @@ class MangaListTransformer extends AbstractTransformer {
*/ */
public function untransform(array $item): FormItem public function untransform(array $item): FormItem
{ {
$reconsuming = $item['status'] === AnilistStatus::REPEATING;
return FormItem::from([ return FormItem::from([
'id' => $item['id'], 'id' => $item['id'],
'mal_id' => $item['media']['idMal'], 'mal_id' => $item['media']['idMal'],
@ -49,8 +52,10 @@ class MangaListTransformer extends AbstractTransformer {
'progress' => $item['progress'], 'progress' => $item['progress'],
'rating' => $item['score'], 'rating' => $item['score'],
'reconsumeCount' => $item['repeat'], 'reconsumeCount' => $item['repeat'],
'reconsuming' => $item['status'] === AnilistStatus::REPEATING, 'reconsuming' => $reconsuming,
'status' => MangaReadingStatus::ANILIST_TO_KITSU[$item['status']], 'status' => $reconsuming
? KitsuStatus::READING
: MangaReadingStatus::ANILIST_TO_KITSU[$item['status']],
'updatedAt' => (new DateTime()) 'updatedAt' => (new DateTime())
->setTimestamp($item['updatedAt']) ->setTimestamp($item['updatedAt'])
->format(DateTime::W3C), ->format(DateTime::W3C),

View File

@ -29,6 +29,7 @@ use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException}; use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
use Throwable; use Throwable;
use const PHP_SAPI;
/** /**
* Kitsu API Authentication * Kitsu API Authentication
@ -83,35 +84,67 @@ final class Auth {
$auth = $this->model->authenticate($username, $password); $auth = $this->model->authenticate($username, $password);
if (FALSE !== $auth) return $this->storeAuth($auth);
}
/**
* Check whether the current user is authenticated
*
* @return boolean
*/
public function isAuthenticated(): bool
{ {
// Set the token in the cache for command line operations return ($this->getAuthToken() !== NULL);
$cacheItem = $this->cache->getItem(K::AUTH_TOKEN_CACHE_KEY);
$cacheItem->set($auth['access_token']);
$cacheItem->save();
// Set the token expiration in the cache
$expireTime = $auth['created_at'] + $auth['expires_in'];
$cacheItem = $this->cache->getItem(K::AUTH_TOKEN_EXP_CACHE_KEY);
$cacheItem->set($expireTime);
$cacheItem->save();
// Set the refresh token in the cache
$cacheItem = $this->cache->getItem(K::AUTH_TOKEN_REFRESH_CACHE_KEY);
$cacheItem->set($auth['refresh_token']);
$cacheItem->save();
// Set the session values
$this->segment->set('auth_token', $auth['access_token']);
$this->segment->set('auth_token_expires', $expireTime);
$this->segment->set('refresh_token', $auth['refresh_token']);
return TRUE;
} }
return FALSE; /**
* Clear authentication values
*
* @return void
*/
public function logout(): void
{
$this->segment->clear();
} }
/**
* Retrieve the authentication token from the session
*
* @return string|false
*/
private function getAuthToken(): ?string
{
$now = time();
if (PHP_SAPI === 'cli')
{
$token = $this->cacheGet(K::AUTH_TOKEN_CACHE_KEY, NULL);
$refreshToken = $this->cacheGet(K::AUTH_TOKEN_REFRESH_CACHE_KEY, NULL);
$expireTime = $this->cacheGet(K::AUTH_TOKEN_EXP_CACHE_KEY);
$isExpired = $now > $expireTime;
}
else
{
$token = $this->segment->get('auth_token', NULL);
$refreshToken = $this->segment->get('refresh_token', NULL);
$isExpired = $now > $this->segment->get('auth_token_expires', $now + 5000);
}
// Attempt to re-authenticate with refresh token
/* if ($isExpired === TRUE && $refreshToken !== NULL)
{
if ($this->reAuthenticate($refreshToken) !== NULL)
{
return (PHP_SAPI === 'cli')
? $this->cacheGet(K::AUTH_TOKEN_CACHE_KEY, NULL)
: $this->segment->get('auth_token', NULL);
}
return NULL;
}*/
return $token;
}
/** /**
* Make the call to re-authenticate with the existing refresh token * Make the call to re-authenticate with the existing refresh token
@ -121,10 +154,15 @@ final class Auth {
* @throws InvalidArgumentException * @throws InvalidArgumentException
* @throws Throwable * @throws Throwable
*/ */
public function reAuthenticate(string $token): bool private function reAuthenticate(string $token): bool
{ {
$auth = $this->model->reAuthenticate($token); $auth = $this->model->reAuthenticate($token);
return $this->storeAuth($auth);
}
private function storeAuth($auth): bool
{
if (FALSE !== $auth) if (FALSE !== $auth)
{ {
// Set the token in the cache for command line operations // Set the token in the cache for command line operations
@ -153,52 +191,15 @@ final class Auth {
return FALSE; return FALSE;
} }
private function cacheGet(string $key, $default = NULL)
/**
* Check whether the current user is authenticated
*
* @return boolean
*/
public function isAuthenticated(): bool
{ {
return ($this->get_auth_token() !== FALSE); $cacheItem = $this->cache->getItem($key);
if ( ! $cacheItem->isHit())
{
return $default;
} }
/** return $cacheItem->get();
* Clear authentication values
*
* @return void
*/
public function logout(): void
{
$this->segment->clear();
}
/**
* Retrieve the authentication token from the session
*
* @return string|false
*/
public function get_auth_token()
{
$now = time();
$token = $this->segment->get('auth_token', FALSE);
$refreshToken = $this->segment->get('refresh_token', FALSE);
$isExpired = time() > $this->segment->get('auth_token_expires', $now + 5000);
// Attempt to re-authenticate with refresh token
/* if ($isExpired && $refreshToken)
{
if ($this->reAuthenticate($refreshToken))
{
return $this->segment->get('auth_token', FALSE);
}
return FALSE;
} */
return $token;
} }
} }
// End of KitsuAuth.php // End of KitsuAuth.php

View File

@ -16,6 +16,7 @@
namespace Aviat\AnimeClient\API\Kitsu; namespace Aviat\AnimeClient\API\Kitsu;
use const PHP_SAPI;
use const Aviat\AnimeClient\SESSION_SEGMENT; use const Aviat\AnimeClient\SESSION_SEGMENT;
use function Amp\Promise\wait; use function Amp\Promise\wait;
@ -69,11 +70,14 @@ trait KitsuTrait {
->getSegment(SESSION_SEGMENT); ->getSegment(SESSION_SEGMENT);
$cache = $this->getContainer()->get('cache'); $cache = $this->getContainer()->get('cache');
$cacheItem = $cache->getItem('kitsu-auth-token'); $cacheItem = $cache->getItem(K::AUTH_TOKEN_CACHE_KEY);
$token = null; $token = null;
if (PHP_SAPI === 'cli' && $cacheItem->isHit())
if ($sessionSegment->get('auth_token') !== NULL && $url !== K::AUTH_URL) {
$token = $cacheItem->get();
}
else if ($url !== K::AUTH_URL && $sessionSegment->get('auth_token') !== NULL)
{ {
$token = $sessionSegment->get('auth_token'); $token = $sessionSegment->get('auth_token');
if ( ! $cacheItem->isHit()) if ( ! $cacheItem->isHit())
@ -82,12 +86,8 @@ trait KitsuTrait {
$cacheItem->save(); $cacheItem->save();
} }
} }
else if ($sessionSegment->get('auth_token') === NULL && $cacheItem->isHit())
{
$token = $cacheItem->get();
}
if (NULL !== $token) if ($token !== NULL)
{ {
$request = $request->setAuth('bearer', $token); $request = $request->setAuth('bearer', $token);
} }

View File

@ -26,8 +26,8 @@ use Aviat\AnimeClient\API\{Anilist, CacheTrait, Kitsu};
use Aviat\AnimeClient\API\Kitsu\KitsuRequestBuilder; use Aviat\AnimeClient\API\Kitsu\KitsuRequestBuilder;
use Aviat\Banker\Pool; use Aviat\Banker\Pool;
use Aviat\Ion\Config; use Aviat\Ion\Config;
use Aviat\Ion\Di\{Container, ContainerAware}; use Aviat\Ion\Di\{Container, ContainerInterface, ContainerAware};
use ConsoleKit\{Command, ConsoleException}; use ConsoleKit\{Colors, Command, ConsoleException};
use ConsoleKit\Widgets\Box; use ConsoleKit\Widgets\Box;
use Laminas\Diactoros\{Response, ServerRequestFactory}; use Laminas\Diactoros\{Response, ServerRequestFactory};
use Monolog\Handler\RotatingFileHandler; use Monolog\Handler\RotatingFileHandler;
@ -44,15 +44,30 @@ abstract class BaseCommand extends Command {
* Echo text in a box * Echo text in a box
* *
* @param string $message * @param string $message
* @param string|int|null $fgColor
* @param string|int|null $bgColor
* @return void * @return void
*/ */
protected function echoBox($message): void public function echoBox(string $message, $fgColor = NULL, $bgColor = NULL): void
{ {
try try
{ {
echo "\n"; $len = strlen($message);
// color message
$message = Colors::colorize($message, $fgColor, $bgColor);
$colorLen = strlen($message);
// create the box
$box = new Box($this->getConsole(), $message); $box = new Box($this->getConsole(), $message);
if ($len !== $colorLen)
{
$box->setPadding((($colorLen - $len) / 2) + 2);
}
$box->write(); $box->write();
echo "\n"; echo "\n";
} }
catch (ConsoleException $e) catch (ConsoleException $e)
@ -61,12 +76,42 @@ abstract class BaseCommand extends Command {
} }
} }
public function echo(string $message): void
{
$this->_line($message);
}
public function echoSuccess(string $message): void
{
$this->_line($message, Colors::GREEN | Colors::BOLD, Colors::BLACK);
}
public function echoWarning(string $message): void
{
$this->_line($message, Colors::YELLOW | Colors::BOLD, Colors::BLACK);
}
public function echoWarningBox(string $message): void
{
$this->echoBox($message, Colors::YELLOW | Colors::BOLD, Colors::BLACK);
}
public function echoError(string $message): void
{
$this->_line($message, Colors::RED | Colors::BOLD, Colors::BLACK);
}
public function echoErrorBox(string $message): void
{
$this->echoBox($message, Colors::RED | Colors::BOLD, Colors::BLACK);
}
/** /**
* Setup the Di container * Setup the Di container
* *
* @return Container * @return Containerinterface
*/ */
protected function setupContainer(): Container public function setupContainer(): ContainerInterface
{ {
$APP_DIR = realpath(__DIR__ . '/../../../app'); $APP_DIR = realpath(__DIR__ . '/../../../app');
$APPCONF_DIR = realpath("{$APP_DIR}/appConf/"); $APPCONF_DIR = realpath("{$APP_DIR}/appConf/");
@ -82,7 +127,7 @@ abstract class BaseCommand extends Command {
$configArray = array_replace_recursive($baseConfig, $config, $overrideConfig); $configArray = array_replace_recursive($baseConfig, $config, $overrideConfig);
$di = static function ($configArray) use ($APP_DIR): Container { $di = static function (array $configArray) use ($APP_DIR): Container {
$container = new Container(); $container = new Container();
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@ -103,9 +148,7 @@ abstract class BaseCommand extends Command {
$container->setLogger($kitsu_request_logger, 'kitsu-request'); $container->setLogger($kitsu_request_logger, 'kitsu-request');
// Create Config Object // Create Config Object
$container->set('config', static function() use ($configArray): Config { $container->set('config', fn () => new Config($configArray));
return new Config($configArray);
});
// Create Cache Object // Create Cache Object
$container->set('cache', static function($container) { $container->set('cache', static function($container) {
@ -115,28 +158,20 @@ abstract class BaseCommand extends Command {
}); });
// Create Aura Router Object // Create Aura Router Object
$container->set('aura-router', static function() { $container->set('aura-router', fn () => new RouterContainer);
return new RouterContainer;
});
// Create Request/Response Objects // Create Request/Response Objects
$container->set('request', static function() { $container->set('request', fn () => ServerRequestFactory::fromGlobals(
return ServerRequestFactory::fromGlobals(
$_SERVER, $_SERVER,
$_GET, $_GET,
$_POST, $_POST,
$_COOKIE, $_COOKIE,
$_FILES $_FILES
); ));
}); $container->set('response', fn () => new Response);
$container->set('response', static function(): Response {
return new Response;
});
// Create session Object // Create session Object
$container->set('session', static function() { $container->set('session', fn () => (new SessionFactory())->newInstance($_COOKIE));
return (new SessionFactory())->newInstance($_COOKIE);
});
// Models // Models
$container->set('kitsu-model', static function($container): Kitsu\Model { $container->set('kitsu-model', static function($container): Kitsu\Model {
@ -175,21 +210,21 @@ abstract class BaseCommand extends Command {
return $model; return $model;
}); });
$container->set('auth', static function($container): Kitsu\Auth { $container->set('auth', fn ($container) => new Kitsu\Auth($container));
return new Kitsu\Auth($container);
});
$container->set('url-generator', static function($container): UrlGenerator { $container->set('url-generator', fn ($container) => new UrlGenerator($container));
return new UrlGenerator($container);
});
$container->set('util', static function($container): Util { $container->set('util', fn ($container) => new Util($container));
return new Util($container);
});
return $container; return $container;
}; };
return $di($configArray); return $di($configArray);
} }
private function _line(string $message, $fgColor = NULL, $bgColor = NULL): void
{
$message = Colors::colorize($message, $fgColor, $bgColor);
$this->getConsole()->writeln($message);
}
} }

View File

@ -0,0 +1,28 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Enum;
use Aviat\Ion\Enum as BaseEnum;
/**
* Types of lists
*/
final class ListType extends BaseEnum {
public const ANIME = 'anime';
public const DRAMA = 'drama';
public const MANGA = 'manga';
}

View File

@ -0,0 +1,28 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Enum;
use Aviat\Ion\Enum as BaseEnum;
/**
* Types of actions when syncing lists from different APIs
*/
final class SyncAction extends BaseEnum {
public const CREATE = 'create';
public const UPDATE = 'update';
public const DELETE = 'delete';
}