Обновление клиента
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\AppInfo;
|
||||
|
||||
use OCA\TwoFactorNextcloudNotification\Event\StateChanged;
|
||||
use OCA\TwoFactorNextcloudNotification\Listener\RegistryUpdater;
|
||||
use OCA\TwoFactorNextcloudNotification\Notification\Notifier;
|
||||
use OCP\AppFramework\App;
|
||||
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||
|
||||
class Application extends App implements IBootstrap {
|
||||
public const APP_ID = 'twofactor_nextcloud_notification';
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct(self::APP_ID);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function register(IRegistrationContext $context): void {
|
||||
$context->registerNotifierService(Notifier::class);
|
||||
$context->registerEventListener(StateChanged::class, RegistryUpdater::class);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function boot(IBootContext $context): void {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\BackgroundJob;
|
||||
|
||||
use OCA\TwoFactorNextcloudNotification\Service\TokenManager;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\BackgroundJob\TimedJob;
|
||||
|
||||
class CleanupTokens extends TimedJob {
|
||||
public function __construct(
|
||||
ITimeFactory $timeFactory,
|
||||
private TokenManager $tokenManager,
|
||||
) {
|
||||
parent::__construct($timeFactory);
|
||||
|
||||
// Run once an hour
|
||||
$this->setInterval(3600);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function run($argument) {
|
||||
$this->tokenManager->cleanupTokens();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\Controller;
|
||||
|
||||
use OCA\TwoFactorNextcloudNotification\Db\Token;
|
||||
use OCA\TwoFactorNextcloudNotification\Exception\TokenExpireException;
|
||||
use OCA\TwoFactorNextcloudNotification\Service\TokenManager;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
|
||||
use OCP\AppFramework\Http\Attribute\PublicPage;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\OCSController;
|
||||
use OCP\IRequest;
|
||||
|
||||
class APIController extends OCSController {
|
||||
public function __construct(
|
||||
string $appName,
|
||||
IRequest $request,
|
||||
private TokenManager $tokenManager,
|
||||
private ?string $userId = null,
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
}
|
||||
|
||||
#[NoAdminRequired]
|
||||
public function approve(int $attemptId): DataResponse {
|
||||
try {
|
||||
$token = $this->tokenManager->getById($attemptId);
|
||||
} catch (DoesNotExistException $e) {
|
||||
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||
} catch (TokenExpireException $e) {
|
||||
return new DataResponse([], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
if ($token->getUserId() !== $this->userId) {
|
||||
return new DataResponse([], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
$token->setStatus(Token::ACCEPTED);
|
||||
$this->tokenManager->update($token);
|
||||
|
||||
return new DataResponse([], Http::STATUS_ACCEPTED);
|
||||
}
|
||||
|
||||
#[NoAdminRequired]
|
||||
public function disapprove(int $attemptId): DataResponse {
|
||||
try {
|
||||
$token = $this->tokenManager->getById($attemptId);
|
||||
} catch (DoesNotExistException $e) {
|
||||
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||
} catch (TokenExpireException $e) {
|
||||
return new DataResponse([], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
if ($token->getUserId() !== $this->userId) {
|
||||
return new DataResponse([], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
$token->setStatus(Token::REJECTED);
|
||||
$this->tokenManager->update($token);
|
||||
|
||||
return new DataResponse([], Http::STATUS_OK);
|
||||
}
|
||||
|
||||
#[PublicPage]
|
||||
public function poll(string $token): DataResponse {
|
||||
try {
|
||||
$token = $this->tokenManager->getByToken($token);
|
||||
} catch (DoesNotExistException $e) {
|
||||
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||
} catch (TokenExpireException $e) {
|
||||
return new DataResponse([], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
if ($token->getStatus() === Token::PENDING) {
|
||||
return new DataResponse(['status' => 'pending']);
|
||||
}
|
||||
if ($token->getStatus() === Token::ACCEPTED) {
|
||||
return new DataResponse(['status' => 'accepted']);
|
||||
}
|
||||
if ($token->getStatus() === Token::REJECTED) {
|
||||
return new DataResponse(['status' => 'rejected']);
|
||||
}
|
||||
|
||||
return new DataResponse([], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\Controller;
|
||||
|
||||
use OCA\TwoFactorNextcloudNotification\Service\StateManager;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\IRequest;
|
||||
use OCP\IUserSession;
|
||||
|
||||
class SettingsController extends Controller {
|
||||
public function __construct(
|
||||
string $appName,
|
||||
IRequest $request,
|
||||
private StateManager $stateManager,
|
||||
private IUserSession $userSession,
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
}
|
||||
|
||||
#[NoAdminRequired]
|
||||
public function setState(bool $state): JSONResponse {
|
||||
$this->stateManager->setState($this->userSession->getUser(), $state);
|
||||
return new JSONResponse([
|
||||
'enabled' => $state
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\Db;
|
||||
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
use OCP\DB\Types;
|
||||
|
||||
/**
|
||||
* @method string getUserId()
|
||||
* @method void setUserId(string $userId)
|
||||
* @method string getToken()
|
||||
* @method void setToken(string $token)
|
||||
* @method int getStatus()
|
||||
* @method void setStatus(int $status)
|
||||
* @method int getTimestamp()
|
||||
* @method void setTimestamp(int $timestamp)
|
||||
*/
|
||||
class Token extends Entity {
|
||||
public const PENDING = 0;
|
||||
public const ACCEPTED = 1;
|
||||
public const REJECTED = 2;
|
||||
|
||||
/** @var string */
|
||||
protected $userId;
|
||||
/** @var string */
|
||||
protected $token;
|
||||
/** @var int */
|
||||
protected $status;
|
||||
/** @var int */
|
||||
protected $timestamp;
|
||||
|
||||
public function __construct() {
|
||||
$this->addType('userId', Types::STRING);
|
||||
$this->addType('token', Types::STRING);
|
||||
$this->addType('status', Types::INTEGER);
|
||||
$this->addType('timestamp', Types::INTEGER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\Db;
|
||||
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\QBMapper;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\Security\ISecureRandom;
|
||||
|
||||
/**
|
||||
* @template-extends QBMapper<Token>
|
||||
*/
|
||||
class TokenMapper extends QBMapper {
|
||||
public function __construct(
|
||||
IDBConnection $db,
|
||||
private ITimeFactory $timeFactory,
|
||||
private ISecureRandom $random,
|
||||
) {
|
||||
parent::__construct($db, 'twofactor_tnn_tokens', Token::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $token
|
||||
* @return Token
|
||||
* @throws DoesNotExistException
|
||||
*/
|
||||
public function getByToken(string $token): Token {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where(
|
||||
$qb->expr()->eq('token', $qb->createNamedParameter($token))
|
||||
);
|
||||
|
||||
return $this->findEntity($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return Token
|
||||
* @throws DoesNotExistException
|
||||
*/
|
||||
public function getById(int $id): Token {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where(
|
||||
$qb->expr()->eq('id', $qb->createNamedParameter($id))
|
||||
);
|
||||
|
||||
return $this->findEntity($qb);
|
||||
}
|
||||
|
||||
public function generate(string $userId): Token {
|
||||
$token = new Token();
|
||||
|
||||
$token->setStatus(Token::PENDING);
|
||||
$token->setUserId($userId);
|
||||
$token->setTimestamp($this->timeFactory->getTime());
|
||||
$token->setToken($this->random->generate(40, ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER));
|
||||
|
||||
/** @var Token $token */
|
||||
$token = $this->insert($token);
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function getTokensForCleanup(): array {
|
||||
// Clear all tokens older than 10 minutes
|
||||
$time = $this->timeFactory->getTime() - (60 * 10);
|
||||
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->lt('timestamp', $qb->createNamedParameter($time)));
|
||||
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\Event;
|
||||
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\IUser;
|
||||
|
||||
class StateChanged extends Event {
|
||||
public function __construct(
|
||||
private IUser $user,
|
||||
private bool $enabled,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IUser
|
||||
*/
|
||||
public function getUser(): IUser {
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled(): bool {
|
||||
return $this->enabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\Exception;
|
||||
|
||||
class TokenExpireException extends \Exception {
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\Listener;
|
||||
|
||||
use OCA\TwoFactorNextcloudNotification\Event\StateChanged;
|
||||
use OCA\TwoFactorNextcloudNotification\Provider\NotificationProvider;
|
||||
use OCP\Authentication\TwoFactorAuth\IRegistry;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\User\Events\PostLoginEvent;
|
||||
|
||||
/**
|
||||
* @template-implements IEventListener<Event|PostLoginEvent>
|
||||
*/
|
||||
class RegistryUpdater implements IEventListener {
|
||||
public function __construct(
|
||||
private IRegistry $registry,
|
||||
private NotificationProvider $provider,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function handle(Event $event): void {
|
||||
if ($event instanceof StateChanged) {
|
||||
if ($event->isEnabled()) {
|
||||
$this->registry->enableProviderFor($this->provider, $event->getUser());
|
||||
} else {
|
||||
$this->registry->disableProviderFor($this->provider, $event->getUser());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\Migration;
|
||||
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
class Version0001Date20180411172140 extends SimpleMigrationStep {
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||
* @param array $options
|
||||
* @return null|ISchemaWrapper
|
||||
*/
|
||||
#[\Override]
|
||||
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) {
|
||||
/**
|
||||
* Dropped by Version4000Date20240802134536
|
||||
* Recreated with oracle compatible names in Version4000Date20240802134537
|
||||
* if (!$schema->hasTable(Application::APP_ID . '_tokens')) {
|
||||
* $table = $schema->createTable(Application::APP_ID . '_tokens');
|
||||
*
|
||||
* $table->addColumn('id', Types::INTEGER, [
|
||||
* 'autoincrement' => true,
|
||||
* 'notnull' => true,
|
||||
* 'length' => 20,
|
||||
* ]);
|
||||
* $table->addColumn('user_id', Types::STRING, [
|
||||
* 'notnull' => true,
|
||||
* 'length' => 64,
|
||||
* ]);
|
||||
* $table->addColumn('token', Types::STRING, [
|
||||
* 'notnull' => true,
|
||||
* 'length' => 40,
|
||||
* ]);
|
||||
* $table->addColumn('timestamp', Types::INTEGER, [
|
||||
* 'notnull' => true,
|
||||
* 'length' => 20,
|
||||
* ]);
|
||||
* $table->addColumn('status', Types::INTEGER, [
|
||||
* 'notnull' => true,
|
||||
* 'length' => 2,
|
||||
* ]);
|
||||
*
|
||||
* $table->setPrimaryKey(['id'], Application::APP_ID . '_tokens_id_idx');
|
||||
* $table->addIndex(['token'], Application::APP_ID . '_tokens_token_idx');
|
||||
*
|
||||
* return $schema;
|
||||
* }
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\Migration;
|
||||
|
||||
use Closure;
|
||||
use OCP\IConfig;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
class Version3004Date20220331145316 extends SimpleMigrationStep {
|
||||
public function __construct(
|
||||
protected IConfig $config,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||
* @param array $options
|
||||
*/
|
||||
#[\Override]
|
||||
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
|
||||
$keys = $this->config->getAppKeys('twofactor_nextcloud_notification');
|
||||
foreach ($keys as $key) {
|
||||
if (str_ends_with($key, '_enabled')) {
|
||||
$this->config->setUserValue(
|
||||
substr($key, 0, -8),
|
||||
'twofactor_nextcloud_notification',
|
||||
'enabled',
|
||||
'1'
|
||||
);
|
||||
|
||||
$this->config->deleteAppValue('twofactor_nextcloud_notification', $key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\Migration;
|
||||
|
||||
use Closure;
|
||||
use OCA\TwoFactorNextcloudNotification\AppInfo\Application;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
class Version4000Date20240802134536 extends SimpleMigrationStep {
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure(): ISchemaWrapper $schemaClosure
|
||||
* @param array $options
|
||||
* @return null|ISchemaWrapper
|
||||
*/
|
||||
#[\Override]
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||
/** @var ISchemaWrapper $schema */
|
||||
$schema = $schemaClosure();
|
||||
|
||||
if ($schema->hasTable(Application::APP_ID . '_tokens')) {
|
||||
$schema->dropTable(Application::APP_ID . '_tokens');
|
||||
return $schema;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\Migration;
|
||||
|
||||
use Closure;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\DB\Types;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
class Version4000Date20240802134537 extends SimpleMigrationStep {
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure(): ISchemaWrapper $schemaClosure
|
||||
* @param array $options
|
||||
* @return null|ISchemaWrapper
|
||||
*/
|
||||
#[\Override]
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||
/** @var ISchemaWrapper $schema */
|
||||
$schema = $schemaClosure();
|
||||
|
||||
if (!$schema->hasTable('twofactor_tnn_tokens')) {
|
||||
$table = $schema->createTable('twofactor_tnn_tokens');
|
||||
|
||||
$table->addColumn('id', Types::INTEGER, [
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
'length' => 20,
|
||||
]);
|
||||
$table->addColumn('user_id', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 64,
|
||||
]);
|
||||
$table->addColumn('token', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 40,
|
||||
]);
|
||||
$table->addColumn('timestamp', Types::INTEGER, [
|
||||
'notnull' => true,
|
||||
'length' => 20,
|
||||
]);
|
||||
$table->addColumn('status', Types::INTEGER, [
|
||||
'notnull' => true,
|
||||
'length' => 2,
|
||||
]);
|
||||
|
||||
$table->setPrimaryKey(['id']);
|
||||
$table->addIndex(['token'], 'twofactor_tnn_token_idx');
|
||||
|
||||
return $schema;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\Notification;
|
||||
|
||||
use OCA\TwoFactorNextcloudNotification\AppInfo\Application;
|
||||
use OCA\TwoFactorNextcloudNotification\Exception\TokenExpireException;
|
||||
use OCA\TwoFactorNextcloudNotification\Service\TokenManager;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\L10N\IFactory;
|
||||
use OCP\Notification\AlreadyProcessedException;
|
||||
use OCP\Notification\INotification;
|
||||
use OCP\Notification\INotifier;
|
||||
use OCP\Notification\UnknownNotificationException;
|
||||
|
||||
class Notifier implements INotifier {
|
||||
public function __construct(
|
||||
protected IFactory $l10nFactory,
|
||||
protected IURLGenerator $urlGenerator,
|
||||
protected TokenManager $tokenManager,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getID(): string {
|
||||
return Application::APP_ID;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getName(): string {
|
||||
return $this->l10nFactory->get(Application::APP_ID)->t('TwoFactor Nextcloud notification');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param INotification $notification
|
||||
* @param string $languageCode The code of the language that should be used to prepare the notification
|
||||
* @return INotification
|
||||
* @throws UnknownNotificationException When the notification was not prepared by a notifier
|
||||
* @throws AlreadyProcessedException When the notification is not needed anymore and should be deleted
|
||||
*/
|
||||
#[\Override]
|
||||
public function prepare(INotification $notification, string $languageCode): INotification {
|
||||
if ($notification->getApp() !== Application::APP_ID ||
|
||||
$notification->getSubject() !== 'login_attempt') {
|
||||
throw new UnknownNotificationException();
|
||||
}
|
||||
|
||||
$attemptId = $notification->getObjectId();
|
||||
|
||||
try {
|
||||
$token = $this->tokenManager->getById((int)$attemptId);
|
||||
} catch (DoesNotExistException|TokenExpireException) {
|
||||
throw new AlreadyProcessedException();
|
||||
}
|
||||
|
||||
if ($token->getUserId() !== $notification->getUser()) {
|
||||
throw new AlreadyProcessedException();
|
||||
}
|
||||
|
||||
$l = $this->l10nFactory->get(Application::APP_ID, $languageCode);
|
||||
$param = $notification->getSubjectParameters();
|
||||
|
||||
$approveAction = $notification->createAction()
|
||||
->setParsedLabel($l->t('Approve'))
|
||||
->setPrimary(true)
|
||||
->setLink(
|
||||
$this->urlGenerator->linkToOCSRouteAbsolute(
|
||||
'twofactor_nextcloud_notification.API.approve',
|
||||
['attemptId' => $attemptId, 'apiVersion' => 'v1'],
|
||||
),
|
||||
'POST'
|
||||
);
|
||||
|
||||
$disapproveAction = $notification->createAction()
|
||||
->setParsedLabel($l->t('Cancel'))
|
||||
->setPrimary(false)
|
||||
->setLink(
|
||||
$this->urlGenerator->linkToOCSRouteAbsolute(
|
||||
'twofactor_nextcloud_notification.API.disapprove',
|
||||
['attemptId' => $attemptId, 'apiVersion' => 'v1'],
|
||||
),
|
||||
'DELETE'
|
||||
);
|
||||
|
||||
$notification->addParsedAction($approveAction)
|
||||
->addParsedAction($disapproveAction)
|
||||
->setPriorityNotification(true)
|
||||
->setParsedSubject(str_replace('{ip}', $param['ip'], $l->t('Login attempt from IP address {ip}')))
|
||||
->setRichSubject(
|
||||
$l->t('Login attempt from IP address {ip}'),
|
||||
[
|
||||
'ip' => [
|
||||
'type' => 'highlight',
|
||||
'id' => $notification->getObjectId(),
|
||||
'name' => $param['ip'],
|
||||
],
|
||||
])
|
||||
->setParsedMessage($l->t('If you are currently trying log in from another device or browser please approve the request. If you are not trying to log in at the moment, you should use the cancel option to abort the login attempt.'))
|
||||
->setRichMessage($l->t('If you are currently trying log in from another device or browser please approve the request. If you are not trying to log in at the moment, you should use the cancel option to abort the login attempt.'))
|
||||
->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath(Application::APP_ID, 'login-dark.svg')))
|
||||
;
|
||||
return $notification;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\Provider;
|
||||
|
||||
use OCA\TwoFactorNextcloudNotification\AppInfo\Application;
|
||||
use OCA\TwoFactorNextcloudNotification\Db\Token;
|
||||
use OCA\TwoFactorNextcloudNotification\Service\StateManager;
|
||||
use OCA\TwoFactorNextcloudNotification\Service\TokenManager;
|
||||
use OCA\TwoFactorNextcloudNotification\Settings\Personal;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\Authentication\TwoFactorAuth\IActivatableByAdmin;
|
||||
use OCP\Authentication\TwoFactorAuth\IDeactivatableByAdmin;
|
||||
use OCP\Authentication\TwoFactorAuth\IPersonalProviderSettings;
|
||||
use OCP\Authentication\TwoFactorAuth\IProvider;
|
||||
use OCP\Authentication\TwoFactorAuth\IProvidesIcons;
|
||||
use OCP\Authentication\TwoFactorAuth\IProvidesPersonalSettings;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUser;
|
||||
use OCP\Template\ITemplate;
|
||||
use OCP\Template\ITemplateManager;
|
||||
|
||||
class NotificationProvider implements IProvider, IProvidesIcons, IProvidesPersonalSettings, IActivatableByAdmin, IDeactivatableByAdmin {
|
||||
public function __construct(
|
||||
private IL10N $l10n,
|
||||
private TokenManager $tokenManager,
|
||||
private StateManager $stateManager,
|
||||
private IInitialState $initialStateService,
|
||||
private IURLGenerator $url,
|
||||
private ITemplateManager $templateManager,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getId(): string {
|
||||
return Application::APP_ID;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDisplayName(): string {
|
||||
return $this->l10n->t('Nextcloud Notification');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDescription(): string {
|
||||
return $this->l10n->t('Authenticate using a device that is already logged in to your account');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getLightIcon(): string {
|
||||
return $this->url->getAbsoluteURL($this->url->imagePath(Application::APP_ID, 'app.svg'));
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDarkIcon(): string {
|
||||
return $this->url->getAbsoluteURL($this->url->imagePath(Application::APP_ID, 'app-dark.svg'));
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getTemplate(IUser $user): ITemplate {
|
||||
$token = $this->tokenManager->generate($user->getUID());
|
||||
|
||||
$template = $this->templateManager->getTemplate(Application::APP_ID, 'challenge');
|
||||
$template->assign('token', $token->getToken());
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function verifyChallenge(IUser $user, string $challenge): bool {
|
||||
try {
|
||||
$token = $this->tokenManager->getByToken($challenge);
|
||||
} catch (DoesNotExistException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->tokenManager->delete($token);
|
||||
|
||||
return $token->getStatus() === Token::ACCEPTED &&
|
||||
$token->getUserId() === $user->getUID();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function isTwoFactorAuthEnabledForUser(IUser $user): bool {
|
||||
return $this->stateManager->getState($user);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getPersonalSettings(IUser $user): IPersonalProviderSettings {
|
||||
return new Personal(
|
||||
$this->initialStateService,
|
||||
$this->templateManager,
|
||||
$this->stateManager->getState($user),
|
||||
);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function enableFor(IUser $user): void {
|
||||
$this->stateManager->setState($user, true);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function disableFor(IUser $user): void {
|
||||
$this->stateManager->setState($user, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\Service;
|
||||
|
||||
use OCA\TwoFactorNextcloudNotification\AppInfo\Application;
|
||||
use OCA\TwoFactorNextcloudNotification\Db\Token;
|
||||
use OCP\IRequest;
|
||||
use OCP\Notification\IManager;
|
||||
|
||||
class NotificationManager {
|
||||
public function __construct(
|
||||
private IManager $manager,
|
||||
private IRequest $request,
|
||||
) {
|
||||
}
|
||||
|
||||
public function clearNotification(Token $token): void {
|
||||
$notification = $this->manager->createNotification();
|
||||
$notification->setApp(Application::APP_ID)
|
||||
->setSubject('login_attempt')
|
||||
->setObject('2fa_id', (string)$token->getId());
|
||||
$this->manager->markProcessed($notification);
|
||||
}
|
||||
|
||||
public function newNotification(Token $token): void {
|
||||
$notification = $this->manager->createNotification();
|
||||
$notification->setApp(Application::APP_ID)
|
||||
->setSubject('login_attempt', [
|
||||
'ip' => $this->request->getRemoteAddress(),
|
||||
])
|
||||
->setObject('2fa_id', (string)$token->getId())
|
||||
->setUser($token->getUserId())
|
||||
->setDateTime(new \DateTime());
|
||||
$this->manager->notify($notification);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\Service;
|
||||
|
||||
use OCA\TwoFactorNextcloudNotification\AppInfo\Application;
|
||||
use OCA\TwoFactorNextcloudNotification\Event\StateChanged;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\IConfig;
|
||||
use OCP\IUser;
|
||||
|
||||
class StateManager {
|
||||
public function __construct(
|
||||
private IEventDispatcher $dispatcher,
|
||||
private IConfig $config,
|
||||
) {
|
||||
}
|
||||
|
||||
public function setState(IUser $user, bool $state): void {
|
||||
$this->config->setUserValue($user->getUID(), Application::APP_ID, 'enabled', $state ? '1' : '0');
|
||||
$this->dispatcher->dispatchTyped(new StateChanged($user, $state));
|
||||
}
|
||||
|
||||
public function getState(IUser $user): bool {
|
||||
return $this->config->getUserValue($user->getUID(), Application::APP_ID, 'enabled', '0') === '1';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\Service;
|
||||
|
||||
use OCA\TwoFactorNextcloudNotification\Db\Token;
|
||||
use OCA\TwoFactorNextcloudNotification\Db\TokenMapper;
|
||||
use OCA\TwoFactorNextcloudNotification\Exception\TokenExpireException;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
|
||||
class TokenManager {
|
||||
public function __construct(
|
||||
private TokenMapper $mapper,
|
||||
private NotificationManager $notificationManager,
|
||||
private ITimeFactory $timeFactory,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $attemptId
|
||||
* @return Token
|
||||
* @throws DoesNotExistException
|
||||
* @throws TokenExpireException
|
||||
*/
|
||||
public function getById(int $attemptId): Token {
|
||||
return $this->validateToken($this->mapper->getById($attemptId));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $token
|
||||
* @return Token
|
||||
* @throws DoesNotExistException
|
||||
* @throws TokenExpireException
|
||||
*/
|
||||
public function getByToken(string $token): Token {
|
||||
return $this->validateToken($this->mapper->getByToken($token));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Token $token
|
||||
*/
|
||||
public function delete(Token $token): void {
|
||||
$this->notificationManager->clearNotification($token);
|
||||
$this->mapper->delete($token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Token $token
|
||||
* @return Token
|
||||
*/
|
||||
public function update(Token $token): Token {
|
||||
$this->notificationManager->clearNotification($token);
|
||||
return $this->mapper->update($token);
|
||||
}
|
||||
|
||||
public function cleanupTokens(): void {
|
||||
$tokens = $this->mapper->getTokensForCleanup();
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
$this->delete($token);
|
||||
}
|
||||
}
|
||||
|
||||
public function generate(string $userId): Token {
|
||||
$token = $this->mapper->generate($userId);
|
||||
$this->notificationManager->newNotification($token);
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Token $token
|
||||
* @return Token
|
||||
* @throws TokenExpireException
|
||||
*/
|
||||
protected function validateToken(Token $token): Token {
|
||||
if (($this->timeFactory->getTime() - $token->getTimestamp()) > 60 * 10) {
|
||||
$this->delete($token);
|
||||
throw new TokenExpireException('Token expired');
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\TwoFactorNextcloudNotification\Settings;
|
||||
|
||||
use OCA\TwoFactorNextcloudNotification\AppInfo\Application;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\Authentication\TwoFactorAuth\IPersonalProviderSettings;
|
||||
use OCP\Template\ITemplate;
|
||||
use OCP\Template\ITemplateManager;
|
||||
|
||||
class Personal implements IPersonalProviderSettings {
|
||||
public function __construct(
|
||||
private IInitialState $initialStateService,
|
||||
private ITemplateManager $templateManager,
|
||||
private bool $enabled,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getBody(): ITemplate {
|
||||
$this->initialStateService->provideInitialState('state', $this->enabled);
|
||||
return $this->templateManager->getTemplate(Application::APP_ID, 'personal');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user