Created an implementation of a token repository that isn't restricted to users and can be re-used for other objects.
This commit is contained in:
@@ -15,6 +15,9 @@
|
||||
* SMTP server password: SMTP_PASSWORD
|
||||
*/
|
||||
return [
|
||||
'debug' => [
|
||||
'tokens' => true,
|
||||
],
|
||||
'organisation' => [
|
||||
'registration' => [
|
||||
'require_approval' => true,
|
||||
|
||||
444
src/Repository/BasicTokenRepository.php
Normal file
444
src/Repository/BasicTokenRepository.php
Normal file
@@ -0,0 +1,444 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* AVSDev UF Organisations (https://avsdev.uk)
|
||||
*
|
||||
* @link https://git.avsdev.uk/avsdev/sprinkle-organisations
|
||||
* @license https://git.avsdev.uk/avsdev/sprinkle-organisations/blob/master/LICENSE.md (LGPL-3.0 License)
|
||||
*/
|
||||
|
||||
namespace UserFrosting\Sprinkle\Organisations\Repository;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use UserFrosting\Sprinkle\Organisations\Repository\Interfaces\TokenOwnerInterface;
|
||||
use UserFrosting\Sprinkle\Core\Database\Models\Model;
|
||||
use UserFrosting\Sprinkle\Core\Util\ClassMapper;
|
||||
|
||||
/**
|
||||
* An abstract class for interacting with a repository of time-sensitive tokens which can be owned by any object.
|
||||
*
|
||||
* Tokens are used, for example, to perform password resets and new account email verifications.
|
||||
*
|
||||
* @author Craig Williams (https://avsdev.uk)
|
||||
*/
|
||||
abstract class BasicTokenRepository
|
||||
{
|
||||
/**
|
||||
* @var ClassMapper
|
||||
*/
|
||||
protected $classMapper;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $algorithm;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $modelIdentifier;
|
||||
|
||||
/**
|
||||
* @var TokenLogger
|
||||
*/
|
||||
protected $tokenLogger;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new TokenRepository object.
|
||||
*
|
||||
* @param ClassMapper $classMapper Maps generic class identifiers to specific class names.
|
||||
* @param string $algorithm The hashing algorithm to use when storing generated tokens.
|
||||
*/
|
||||
public function __construct(ClassMapper $classMapper, $algorithm = 'sha512', $tokenLogger = null, $debug = false)
|
||||
{
|
||||
$this->classMapper = $classMapper;
|
||||
$this->algorithm = $algorithm;
|
||||
$this->tokenLogger = ($debug ? $tokenLogger : null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new token.
|
||||
*
|
||||
* @param TokenOwnerInterface $tokenOwner The object to associate with this token, be it an organisation, user, group etc.
|
||||
* @param int $timeout The time, in seconds, after which this token should expire.
|
||||
*
|
||||
* @return Model The model (PasswordReset, Verification, etc) object that stores the token.
|
||||
*/
|
||||
public function create(TokenOwnerInterface $tokenOwner, $timeout)
|
||||
{
|
||||
// Remove any previous tokens for this tokenOwner
|
||||
$this->removeExisting($tokenOwner);
|
||||
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Creating new token for {{owner}} which expires in {{timeout}} seconds', [
|
||||
'tokenOwner' => $tokenOwner,
|
||||
'timeout' => $timeout
|
||||
]);
|
||||
}
|
||||
|
||||
// Compute expiration time
|
||||
$expiresAt = Carbon::now()->addSeconds($timeout);
|
||||
|
||||
$model = $this->classMapper->createInstance($this->modelIdentifier);
|
||||
|
||||
// Generate a random token
|
||||
$model->setToken($this->generateRandomToken());
|
||||
|
||||
// Hash the password reset token for the stored version
|
||||
$hash = hash($this->algorithm, $model->getToken());
|
||||
|
||||
$model->fill([
|
||||
'owner_id' => $tokenOwner->getId(),
|
||||
'hash' => $hash,
|
||||
'completed' => false,
|
||||
'expires_at' => ($timeout >= 0 ? $expiresAt : null),
|
||||
]);
|
||||
|
||||
$model->save();
|
||||
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Completed new token', ['model' => $model]);
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a specified token by removing it from the database.
|
||||
*
|
||||
* @param int $token The token to remove.
|
||||
*
|
||||
* @return Model|false
|
||||
*/
|
||||
public function cancel($token)
|
||||
{
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Cancelling token {{token}}', [
|
||||
'token' => $token
|
||||
]);
|
||||
}
|
||||
|
||||
// Hash the password reset token for the stored version
|
||||
$hash = hash($this->algorithm, $token);
|
||||
|
||||
// Find an incomplete reset request for the specified hash
|
||||
$model = $this->classMapper->getClassMapping($this->modelIdentifier)::query()
|
||||
->where('hash', $hash)
|
||||
->where('completed', false)
|
||||
->first();
|
||||
|
||||
if ($model === null) {
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->warn('Token not found!');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Deleting matched model', [
|
||||
'model' => $model
|
||||
]);
|
||||
}
|
||||
|
||||
$model->delete();
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes a token-based process, invoking updateTokenOwner() in the child object to do the actual action.
|
||||
*
|
||||
* @param int $token The token to complete.
|
||||
* @param mixed[] $params An optional list of parameters to pass to updateUser().
|
||||
*
|
||||
* @return Model|false
|
||||
*/
|
||||
public function complete($token, $params = [])
|
||||
{
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Completing token for {{token}}', [
|
||||
'token' => $token
|
||||
]);
|
||||
}
|
||||
|
||||
// Hash the token for the stored version
|
||||
$hash = hash($this->algorithm, $token);
|
||||
|
||||
// Find an unexpired, incomplete token for the specified hash
|
||||
$model = $this->classMapper->getClassMapping($this->modelIdentifier)::query()
|
||||
->where('hash', $hash)
|
||||
->where('completed', false)
|
||||
->where(function($query) {
|
||||
return $query->where('expires_at', '>', Carbon::now())->orWhereNull('expires_at');
|
||||
})
|
||||
->first();
|
||||
|
||||
if ($model === null) {
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->warn('Token not found!');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Found:', [
|
||||
'model' => $model
|
||||
]);
|
||||
}
|
||||
|
||||
$ret = $this->updateTokenOwner($model->owner_id, $model, $params);
|
||||
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Return of updateTokenOwner: {{ret}}', [
|
||||
'ret' => $ret
|
||||
]);
|
||||
}
|
||||
|
||||
$model->fill([
|
||||
'completed' => true,
|
||||
'completed_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
$model->save();
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes a token-based process using the owner instead of the token,
|
||||
* invoking updateTokenOwner() in the child object to do the actual action.
|
||||
*
|
||||
* @param int $token The token to complete.
|
||||
* @param mixed[] $params An optional list of parameters to pass to updateUser().
|
||||
*
|
||||
* @return Model|false
|
||||
*/
|
||||
public function completeForOwner(TokenOwnerInterface $tokenOwner, $params = [])
|
||||
{
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Completing token for {{owner}}', [
|
||||
'owner' => $tokenOwner
|
||||
]);
|
||||
}
|
||||
|
||||
// Find an unexpired, incomplete tokens for the specified owner.
|
||||
// Using first() works because owners can only have at most 1 active token
|
||||
$model = $this->classMapper->getClassMapping($this->modelIdentifier)::query()
|
||||
->where('owner_id', $tokenOwner->getId())
|
||||
->where('completed', false)
|
||||
->where(function($query) {
|
||||
return $query->where('expires_at', '>', Carbon::now())->orWhereNull('expires_at');
|
||||
})
|
||||
->first();
|
||||
|
||||
if ($model === null) {
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->warn('Token not found!');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Found:', [
|
||||
'model' => $model
|
||||
]);
|
||||
}
|
||||
|
||||
$ret = $this->updateTokenOwner($model->owner_id, $model, $params);
|
||||
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Return of updateTokenOwner: {{ret}}', [
|
||||
'ret' => $ret
|
||||
]);
|
||||
}
|
||||
|
||||
$model->fill([
|
||||
'completed' => true,
|
||||
'completed_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
$model->save();
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a specified token owner has an incomplete and unexpired token.
|
||||
*
|
||||
* @param TokenOwnerInterface $tokenOwner The token owner object to look up.
|
||||
* @param int $token Optionally, try to match a specific token.
|
||||
*
|
||||
* @return Model|false
|
||||
*/
|
||||
public function exists(TokenOwnerInterface $tokenOwner, $token = null)
|
||||
{
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Searching for token for {{owner}} (with optional {{token}})', [
|
||||
'owner' => $tokenOwner,
|
||||
'token' => $token
|
||||
]);
|
||||
}
|
||||
|
||||
$model = $this->classMapper->getClassMapping($this->modelIdentifier)::query()
|
||||
->where('owner_id', $tokenOwner->id)
|
||||
->where('completed', false)
|
||||
->where(function($query) {
|
||||
return $query->where('expires_at', '>', Carbon::now())->orWhereNull('expires_at');
|
||||
});
|
||||
|
||||
if ($token) {
|
||||
// get token hash
|
||||
$hash = hash($this->algorithm, $token);
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Token hash: {{hash}}', [
|
||||
'hash' => $hash
|
||||
]);
|
||||
}
|
||||
$model->where('hash', $hash);
|
||||
}
|
||||
|
||||
$result = $model->first() ?: false;
|
||||
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Found:', [
|
||||
'result' => $result
|
||||
]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an unexpired and un-completed token for the specified owner.
|
||||
*
|
||||
* @param TokenOwnerInterface $tokenOwner The token owner object to look up.
|
||||
* @param int $token Optionally, try to match a specific token.
|
||||
*
|
||||
* @return owner_id|null
|
||||
*/
|
||||
public function findOwner($token)
|
||||
{
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Searching token for owner of {{token}}', [
|
||||
'token' => $token
|
||||
]);
|
||||
}
|
||||
|
||||
// get token hash
|
||||
$hash = hash($this->algorithm, $token);
|
||||
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Token hash: {{hash}}', [
|
||||
'hash' => $hash
|
||||
]);
|
||||
}
|
||||
|
||||
$model = $this->classMapper->getClassMapping($this->modelIdentifier)::query()
|
||||
->where('hash', $hash)
|
||||
->where('completed', false)
|
||||
->where(function($query) {
|
||||
return $query->where('expires_at', '>', Carbon::now())->orWhereNull('expires_at');
|
||||
})
|
||||
->first();
|
||||
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Found:', [
|
||||
'model' => $model
|
||||
]);
|
||||
}
|
||||
|
||||
return $model ? $model->owner_id : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all existing tokens from the database for a particular owner.
|
||||
*
|
||||
* @param TokenOwnerInterface $tokenOwner
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function removeExisting(TokenOwnerInterface $tokenOwner)
|
||||
{
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Completing token for {{owner}}', [
|
||||
'owner' => $tokenOwner
|
||||
]);
|
||||
}
|
||||
|
||||
$result = $this->classMapper->getClassMapping($this->modelIdentifier)::query()
|
||||
->where('owner_id', $tokenOwner->getId())
|
||||
->delete();
|
||||
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Deletion result: {{result}}', [
|
||||
'result' => $result
|
||||
]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all expired tokens from the database.
|
||||
*
|
||||
* @return bool|null
|
||||
*/
|
||||
public function removeExpired()
|
||||
{
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Removing expired tokens...');
|
||||
}
|
||||
|
||||
$result = $this->classMapper->getClassMapping($this->modelIdentifier)::query()
|
||||
->where('completed', false)
|
||||
->whereNotNull('expires_at')
|
||||
->where('expires_at', '<', Carbon::now())
|
||||
->delete();
|
||||
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Deletion result: {{result}}', [
|
||||
'result' => $result
|
||||
]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new random token.
|
||||
*
|
||||
* This generates a token to use for verifying anything you like, with a unique reference.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function generateRandomToken()
|
||||
{
|
||||
do {
|
||||
$gen = md5(uniqid(mt_rand(), false));
|
||||
} while ($this->classMapper->getClassMapping($this->modelIdentifier)::query()
|
||||
->where('hash', hash($this->algorithm, $gen))
|
||||
->first());
|
||||
|
||||
if ($this->tokenLogger) {
|
||||
$this->tokenLogger->debug('Generated token {{token}}', [
|
||||
'token' => $gen
|
||||
]);
|
||||
}
|
||||
|
||||
return $gen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the owner of the token during the token completion process.
|
||||
*
|
||||
* This method is called during complete(), and is a way for concrete implementations to modify the owner.
|
||||
*
|
||||
* @param integer $owner_id The id of the token owner
|
||||
* @param mixed[] $args
|
||||
*
|
||||
* @return mixed[] $args the list of parameters that were supplied to the call to `complete()`
|
||||
*/
|
||||
abstract protected function updateTokenOwner($owner_id, $model, $args);
|
||||
}
|
||||
25
src/Repository/Interfaces/TokenOwnerInterface.php
Normal file
25
src/Repository/Interfaces/TokenOwnerInterface.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* AVSDev UF Organisations (https://avsdev.uk)
|
||||
*
|
||||
* @link https://git.avsdev.uk/avsdev/sprinkle-organisations
|
||||
* @license https://git.avsdev.uk/avsdev/sprinkle-organisations/blob/master/LICENSE.md (LGPL-3.0 License)
|
||||
*/
|
||||
|
||||
namespace UserFrosting\Sprinkle\Organisations\Repository\Interfaces;
|
||||
|
||||
/**
|
||||
* Token Owner Interface.
|
||||
*
|
||||
* Represents a Token Owner object. The only requirement is that it must provide a unique id per object.
|
||||
*/
|
||||
interface TokenOwnerInterface
|
||||
{
|
||||
/**
|
||||
* Returns a unique ID for this Token Owner
|
||||
*
|
||||
* @return integer|string
|
||||
*/
|
||||
public function getId();
|
||||
}
|
||||
@@ -10,9 +10,13 @@
|
||||
namespace UserFrosting\Sprinkle\Organisations\ServicesProvider;
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use Monolog\Formatter\LineFormatter;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use UserFrosting\Sprinkle\Core\Log\MixedFormatter;
|
||||
use UserFrosting\Sprinkle\Organisations\Database\Models\Interfaces\OrganisationInterface;
|
||||
use UserFrosting\Sprinkle\Organisations\Twig\OrganisationsExtension;
|
||||
use UserFrosting\Sprinkle\Organisations\Repository\OrganisationApprovalRepository;
|
||||
@@ -104,6 +108,28 @@ class ServicesProvider
|
||||
return $view;
|
||||
});
|
||||
|
||||
/*
|
||||
* Token logging with Monolog.
|
||||
*
|
||||
* Extend this service to push additional handlers onto the 'tokens' log stack.
|
||||
*
|
||||
* @return \Monolog\Logger
|
||||
*/
|
||||
$container['tokenLogger'] = function ($c) {
|
||||
$logger = new Logger('tokens');
|
||||
|
||||
$logFile = $c->locator->findResource('log://userfrosting.log', true, true);
|
||||
|
||||
$handler = new StreamHandler($logFile);
|
||||
|
||||
$formatter = new MixedFormatter(null, null, true);
|
||||
|
||||
$handler->setFormatter($formatter);
|
||||
$logger->pushHandler($handler);
|
||||
|
||||
return $logger;
|
||||
};
|
||||
|
||||
/*
|
||||
* Returns a callback that handles merging any organisation objects.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user