Archived
1
0
Fork 0

Compare commits

...

34 commits
v1.1 ... dev

Author SHA1 Message Date
50464bce0d Merge branch '37-admin-impersonate-user' into 'dev'
Impersonate users

Closes #37

See merge request pnx/httpcb!34
2023-05-01 15:49:58 +00:00
a757e95277 app/views/_common/_components/navigation.volt: show both users if the user are impersonating another user. 2023-05-01 15:49:42 +00:00
bf940f9523 app/library/Auth.php: Adding getImpersonator() 2023-05-01 15:49:42 +00:00
6dc1d0fb87 app/library/Auth.php: Fix typo. 2023-05-01 15:49:42 +00:00
0634db6d0c app/views/backend/user/index.volt: Add icon link to impersonate users 2023-05-01 15:49:42 +00:00
24edb5cb59 app/config/routes.yml: add impersonate route. 2023-05-01 15:49:42 +00:00
4ac770f733 app/controllers/backend/UserController.php: adding impersonateAction() 2023-05-01 15:49:42 +00:00
69fd7a6e19 app/library/Auth.php: adding impersonate() and impersonateClear() methods 2023-05-01 15:49:42 +00:00
6439a83edb app/listeners/ActivityLog.php: Adding onImpersonate listener 2023-05-01 15:49:42 +00:00
be4950ff88 Style fixes. 2023-04-30 16:57:42 +02:00
a7a59b690a app/library/Services.php: in _initSharedRouter() setup not found route. 2023-04-30 16:46:53 +02:00
dd140ccd8a app/library/Services.php: in _initSharedRouter() Remove default routes 2023-04-30 16:46:04 +02:00
d2e7d6b670 app/config/routes.yml: define all routes and use shorthand path fields. 2023-04-30 16:44:36 +02:00
8c9455a2d5 Merge branch '38-admin-send-activation-password-resets-to-email' into dev 2022-08-28 17:49:22 +02:00
151daa8529 app/controllers/backend/UserController.php: typo fix. 2022-08-28 17:49:03 +02:00
d3e52269cd app/views/backend/user/form.volt: only show "Send activation email" button for suspended users. 2022-08-28 17:48:25 +02:00
8048b3fda8 app/controllers/backend/UserController.php: in activationEmailAction() only send activation email to suspended users. 2022-08-28 17:47:56 +02:00
4ff351d39d app/models/Data/User.php: Adding isSuspended() 2022-08-28 17:46:16 +02:00
1b57749a91 app/views/backend/user/index.volt: Show badge. for status. 2022-08-28 17:38:00 +02:00
c07423f600 app/controllers/ControllerBase.php: set namespace in _forward404() 2022-08-28 17:37:30 +02:00
7315885877 app/config/local.sample.yml: minor fix. 2022-08-28 17:37:26 +02:00
ff15ee697c app/config/routes.yml: Add "status" to the url for backend-user-status route. 2022-08-28 17:36:50 +02:00
2e98191d56 app/controllers/ApiController.php: need to include App\Model\Data\User 2022-08-28 17:36:50 +02:00
0f5f42ca92 app/views/backend/user/form.volt: Add button for sending activation email.
# Conflicts:
#	app/views/backend/user/form.volt
2022-08-28 17:36:31 +02:00
14eb4a9137 app/controllers/backend/UserController.php: Adding activationEmailAction() 2022-08-28 17:36:31 +02:00
e47aa5188e app/library/Services.php: Attach AuthEmailListener to listen to auth events. 2022-08-28 17:35:58 +02:00
5b9d64c09e app/config/routes.yml: Add backend-user-activation-email route 2022-08-28 17:35:58 +02:00
ad2952f9fc app/listeners/ActivityLog.php: Adding onSentActivation 2022-08-28 17:35:58 +02:00
8d74cb2f06 Adding app/listeners/AuthEmailListener.php 2022-08-28 17:35:58 +02:00
6aeaf74a2f package.json: set to version 1.1.0 as npm does not like 1.1 2022-08-28 17:20:53 +02:00
e56c8f37ea app/library/Services.php: in _initSharedModelsMetadata() must pass a factory to adapter in phalcon 4 2022-08-28 17:14:04 +02:00
d7af32a1d7 app/library/Services.php: in _initSession() must pass a factory to adapter in phalcon 4 2022-08-28 17:13:19 +02:00
ee5c7719cb app/listeners/ActivityLog.php: pass true to getClientAddress() so it looks for X-Forwarded-For header. 2022-08-28 16:41:41 +02:00
fa28394a83 composer.json: require ext-mbstring 2022-08-28 16:41:41 +02:00
53 changed files with 398 additions and 193 deletions

View file

@ -30,7 +30,7 @@ session:
statsKey: _httpcb_sess_idx statsKey: _httpcb_sess_idx
prefix: _httpcb_sess_ prefix: _httpcb_sess_
#sendgrid #sendgrid:
#key: value #key: value
# OAuth # OAuth

View file

@ -4,58 +4,61 @@ router:
routes: routes:
home-route: home-route:
pattern: '/' pattern: '/'
path: path: Index::index
controller: index
action: index
about-route: about-route:
pattern: '/about' pattern: '/about'
path: path: Index::about
controller: index
action: about # Callbacks
cb-list:
pattern: '/callback/list'
path: Callback::list
cb-new:
pattern: '/callback/new'
path: Callback::new
cb-created: cb-created:
pattern: '/callback/created/{id}' pattern: '/callback/created/{id}'
path: path: Callback::created
controller: callback cb-show:
action: created pattern: '/callback/show/{id}'
path: Callback::show
cb-endpoint: cb-endpoint:
pattern: '/cb/{id}/:params' pattern: '/cb/{id}/:params'
path: path: Api::endpoint
controller: api
action: endpoint # login
login: login:
pattern: '/login' pattern: '/login'
path: path: Auth::index
controller: auth
action: index
logout: logout:
pattern: '/logout' pattern: '/logout'
path: path: Auth::logout
controller: auth
action: logout
oauth: oauth:
pattern: '/login/{strategy:([a-z]+)}/:params' pattern: '/login/{strategy:([a-z]+)}/:params'
path: path: Auth::oauth
controller: auth
action: oauth
oauth-disconnect: oauth-disconnect:
pattern: '/oauth/{provider:([a-z]+)}/disconnect' pattern: '/oauth/{provider:([a-z]+)}/disconnect'
path: 'User::oauthdisconnect' path: 'User::oauthdisconnect'
oauth-disconnect-confirm: oauth-disconnect-confirm:
pattern: '/oauth/{provider:([a-z]+)}/disconnect/{confirm}' pattern: '/oauth/{provider:([a-z]+)}/disconnect/{confirm}'
path: 'User::oauthdisconnect' path: 'User::oauthdisconnect'
# User
user-register: user-register:
pattern: '/register' pattern: '/register'
path: Auth::register path: Auth::register
user-settings: user-settings:
pattern: '/settings' pattern: '/settings'
path: path: User::settings
controller: user user-activity-log:
action: settings pattern: '/user/activity'
path: User::activity
activation-link: activation-link:
pattern: '/activate/{link}' pattern: '/activate/{link}'
path: path: Api::activationlink
controller: api
action: activationlink
# Backend # Backend
backend-home: backend-home:
@ -70,12 +73,15 @@ router:
backend-user-edit: backend-user-edit:
pattern: '/admin/user/{id:([0-9]+)}' pattern: '/admin/user/{id:([0-9]+)}'
path: backend::user::edit path: backend::user::edit
backend-user-impersonate:
pattern: '/admin/impersonate/{id:([0-9]+)}'
path: backend::user::impersonate
backend-user-activation-email:
pattern: '/admin/user/{id:([0-9]+)}/activation'
path: backend::user::activation-email
backend-user-status: backend-user-status:
pattern: '/admin/user/{id:([0-9]+)}/{type}' pattern: '/admin/user/{id:([0-9]+)}/status/{type}'
path: path: backend::user::status
module: backend
controller: user
action: status
backend-log: backend-log:
pattern: '/admin/log{page:/?([0-9]+)?}' pattern: '/admin/log{page:/?([0-9]+)?}'
path: backend::log::index path: backend::log::index

View file

@ -6,6 +6,7 @@ use App\Controller\ControllerBase,
App\Model\Data\Callback as CallbackModel, App\Model\Data\Callback as CallbackModel,
App\Model\Data\Request as RequestModel, App\Model\Data\Request as RequestModel,
App\Model\Data\RequestMeta as RequestMetaModel, App\Model\Data\RequestMeta as RequestMetaModel,
App\Model\Data\User,
App\Model\Data\UserActivation; App\Model\Data\UserActivation;
class ApiController extends ControllerBase class ApiController extends ControllerBase

View file

@ -118,8 +118,11 @@ class AuthController extends ControllerBase
} }
$user = new User(); $user = new User();
$user->assign($data->toArray(), null, $user->assign(
[ 'email', 'username', 'firstname', 'lastname' ]); $data->toArray(),
null,
['email', 'username', 'firstname', 'lastname']
);
$form = new RegistrationForm($user); $form = new RegistrationForm($user);

View file

@ -55,7 +55,8 @@ class CallbackController extends ControllerBase
return $this->response->redirect(array( return $this->response->redirect(array(
'for' => 'cb-created', 'for' => 'cb-created',
'id' => $callback->getPublicId())); 'id' => $callback->getPublicId()
));
} else { } else {
foreach ($callback->getMessages() as $msg) { foreach ($callback->getMessages() as $msg) {
$this->flash->error($msg); $this->flash->error($msg);
@ -71,8 +72,6 @@ class CallbackController extends ControllerBase
$msg .= '</ul>'; $msg .= '</ul>';
$this->flash->message('error', $msg); $this->flash->message('error', $msg);
} }
} }
$this->view->form = $form; $this->view->form = $form;
@ -85,7 +84,6 @@ class CallbackController extends ControllerBase
{ {
$row = CallbackModel::get($id); $row = CallbackModel::get($id);
if (!$row) { if (!$row) {
} }
$this->view->id = $id; $this->view->id = $id;
} }

View file

@ -18,6 +18,7 @@ class ControllerBase extends Controller
protected function _forward404() protected function _forward404()
{ {
$this->dispatcher->forward(array( $this->dispatcher->forward(array(
'namespace' => 'App\\Controller',
'controller' => 'error', 'controller' => 'error',
'action' => 'show404' 'action' => 'show404'
)); ));

View file

@ -15,4 +15,3 @@ class IndexController extends ControllerBase
{ {
} }
} }

View file

@ -44,8 +44,11 @@ class UserController extends ControllerBase
]); ]);
// Send the email. // Send the email.
$this->di->getMail()->send('Httpcb password activation', $this->di->getMail()->send(
$user->getEmail(), $content); 'Httpcb password activation',
$user->getEmail(),
$content
);
$msg = "For security reasons. Before a password can be created " $msg = "For security reasons. Before a password can be created "
. "a email has been sent to <strong>{$user->getEmail()}</strong> with " . "a email has been sent to <strong>{$user->getEmail()}</strong> with "

View file

@ -97,4 +97,33 @@ class UserController extends \Phalcon\Mvc\Controller
$this->flash->success('The account was: ' . $status); $this->flash->success('The account was: ' . $status);
$this->response->redirect('/admin'); $this->response->redirect('/admin');
} }
public function activationEmailAction($id)
{
$user = User::findFirstById($id);
if ($user) {
if ($user->isSuspended()) {
$this->eventsManager->fire('auth:onSentActivation', $user);
$this->flash->success('Activation email sent to: ' . $user->email);
} else {
$this->flash->error('Only suspended users can be sent activation emails.');
}
} else {
$this->flash->error('Invalid user: ' . $id);
}
$this->response->redirect('/admin');
}
public function impersonateAction($id)
{
$user = User::findFirstById($id);
try {
$this->auth->impersonate($user);
$this->response->redirect('/');
} catch (\Exception $ex) {
$this->flash->error($ex->getMessage());
$this->response->redirect('/admin');
}
}
} }

View file

@ -7,12 +7,14 @@ use Phalcon\Forms\Form;
/** /**
* Element types * Element types
*/ */
use Phalcon\Forms\Element\Text; use Phalcon\Forms\Element\Text;
use Phalcon\Forms\Element\Submit; use Phalcon\Forms\Element\Submit;
/** /**
* Validators * Validators
*/ */
use Phalcon\Validation\Validator\StringLength; use Phalcon\Validation\Validator\StringLength;
class CallbackCreate extends Form class CallbackCreate extends Form

View file

@ -7,6 +7,7 @@ use Phalcon\Forms\Form;
/** /**
* Element types * Element types
*/ */
use Phalcon\Forms\Element\Text; use Phalcon\Forms\Element\Text;
use Phalcon\Forms\Element\Password; use Phalcon\Forms\Element\Password;
use Phalcon\Forms\Element\Submit; use Phalcon\Forms\Element\Submit;
@ -14,6 +15,7 @@ use Phalcon\Forms\Element\Submit;
/** /**
* Validators * Validators
*/ */
use Phalcon\Validation\Validator\PresenceOf; use Phalcon\Validation\Validator\PresenceOf;
use Phalcon\Validation\Validator\Email as EmailValidator; use Phalcon\Validation\Validator\Email as EmailValidator;
use Phalcon\Validation\Validator\StringLength; use Phalcon\Validation\Validator\StringLength;

View file

@ -5,16 +5,19 @@ namespace App\Form;
/** /**
* Models * Models
*/ */
use App\Model\Data\User; use App\Model\Data\User;
/** /**
* Phalcon Form * Phalcon Form
*/ */
use Httpcb\Form as FormBase; use Httpcb\Form as FormBase;
/** /**
* Element types * Element types
*/ */
use Phalcon\Forms\Element\Text, use Phalcon\Forms\Element\Text,
Phalcon\Forms\Element\Password, Phalcon\Forms\Element\Password,
Phalcon\Forms\Element\Submit; Phalcon\Forms\Element\Submit;
@ -22,6 +25,7 @@ use Phalcon\Forms\Element\Text,
/** /**
* Validators * Validators
*/ */
use Phalcon\Validation, use Phalcon\Validation,
Phalcon\Validation\Validator\Callback as CallbackValidator, Phalcon\Validation\Validator\Callback as CallbackValidator,
Phalcon\Validation\Validator\Alnum as AlnumValidator, Phalcon\Validation\Validator\Alnum as AlnumValidator,
@ -54,7 +58,9 @@ class Registration extends FormBase
'messageMinimum' => 'Username must be at least :min characters long.', 'messageMinimum' => 'Username must be at least :min characters long.',
]), ]),
new CallbackValidator([ new CallbackValidator([
'callback' => function($data) { return User::findFirstByUsername($data['username']) === false; }, 'callback' => function ($data) {
return User::findFirstByUsername($data['username']) === false;
},
'message' => 'The username already exists.', 'message' => 'The username already exists.',
'attribute' => 'username', 'attribute' => 'username',
]) ])
@ -87,7 +93,9 @@ class Registration extends FormBase
$email->addValidators([ $email->addValidators([
new CallbackValidator([ new CallbackValidator([
'callback' => function($data) { return User::findFirstByEmail($data['email']) === false; }, 'callback' => function ($data) {
return User::findFirstByEmail($data['email']) === false;
},
'message' => 'This email already exist.', 'message' => 'This email already exist.',
]) ])
]); ]);

View file

@ -5,16 +5,19 @@ namespace App\Form;
/** /**
* Models * Models
*/ */
use App\Model\Data\User as UserModel; use App\Model\Data\User as UserModel;
/** /**
* Form * Form
*/ */
use Httpcb\Form as FormBase; use Httpcb\Form as FormBase;
/** /**
* Element types * Element types
*/ */
use Phalcon\Forms\Element\Text, use Phalcon\Forms\Element\Text,
Phalcon\Forms\Element\Password, Phalcon\Forms\Element\Password,
Phalcon\Forms\Element\Submit; Phalcon\Forms\Element\Submit;
@ -22,6 +25,7 @@ use Phalcon\Forms\Element\Text,
/** /**
* Validators * Validators
*/ */
use Phalcon\Validation\Validator\Callback as CallbackValidator, use Phalcon\Validation\Validator\Callback as CallbackValidator,
Phalcon\Validation\Validator\Uniqueness as UniquenessValidator, Phalcon\Validation\Validator\Uniqueness as UniquenessValidator,
Phalcon\Validation\Validator\Alnum as AlnumValidator, Phalcon\Validation\Validator\Alnum as AlnumValidator,

View file

@ -70,7 +70,6 @@ class Acl
if ($def instanceof Config) { if ($def instanceof Config) {
$inherits = $def->get('inherits'); $inherits = $def->get('inherits');
$description = $def->get('description'); $description = $def->get('description');
} }
$role = new Role($name, $description); $role = new Role($name, $description);

View file

@ -10,6 +10,7 @@ use App\Model\Data\User,
class Auth extends Injectable class Auth extends Injectable
{ {
const SESSION_KEY = 'auth'; const SESSION_KEY = 'auth';
const IMPERSONATOR_ID = 'auth.impersonator';
/** /**
* Login using email/user + password combination. * Login using email/user + password combination.
@ -63,8 +64,11 @@ class Auth extends Injectable
$this->setIdentity($user->getId()); $this->setIdentity($user->getId());
$this->eventsManager->fire('auth:onLogin', $this, $this->eventsManager->fire(
"OAuth {$data->getProvider()}"); 'auth:onLogin',
$this,
"OAuth {$data->getProvider()}"
);
return new Result(Result::SUCCESS); return new Result(Result::SUCCESS);
@ -83,6 +87,40 @@ class Auth extends Injectable
$this->eventsManager->fire('auth:onLogin', $this, 'System'); $this->eventsManager->fire('auth:onLogin', $this, 'System');
} }
public function getImpersonator()
{
$id = $this->session->get(self::IMPERSONATOR_ID);
return $id !== null ? User::findFirst($id) : null;
}
/**
* Impersonate a user
*
* @param User $user
*/
public function impersonate(User $user)
{
$current = $this->getIdentity();
if ($current === null) {
throw new \InvalidArgumentException("Need to be authenticated to be able to impersonate someone");
}
if ($current->getId() === $user->getId()) {
// Same user
throw new \DomainException("Can't impersonate yourself");
}
$this->session->set(self::IMPERSONATOR_ID, $current->getId());
$this->setIdentity($user->getId());
$this->eventsManager->fire('auth:onImpersonate', $this, $current);
}
public function impersonateClear($imp_id)
{
$this->session->remove(self::IMPERSONATOR_ID);
$this->session->set(self::SESSION_KEY, $imp_id);
}
/** /**
* @param $identity * @param $identity
* @return Auth * @return Auth
@ -132,7 +170,12 @@ class Auth extends Injectable
*/ */
public function clearIdentity() public function clearIdentity()
{ {
$imp_id = $this->session->get(self::IMPERSONATOR_ID);
if ($imp_id !== null) {
$this->impersonateClear($imp_id);
} else {
$this->session->remove(self::SESSION_KEY); $this->session->remove(self::SESSION_KEY);
}
return $this; return $this;
} }
} }

View file

@ -2,7 +2,8 @@
namespace Httpcb; namespace Httpcb;
class Debug { class Debug
{
public static function dump($var, $label = null, $echo = true) public static function dump($var, $label = null, $echo = true)
{ {

View file

@ -53,7 +53,10 @@ class Form extends FormBase
$xhtml .= sprintf( $xhtml .= sprintf(
'<label class="%s" for="%s">%s</label>', '<label class="%s" for="%s">%s</label>',
$opt['label-class'], $ele->getName(), $ele->getLabel()); $opt['label-class'],
$ele->getName(),
$ele->getLabel()
);
} }
$xhtml .= '<div class="' . implode(' ', $opt['class']) . '">' $xhtml .= '<div class="' . implode(' ', $opt['class']) . '">'

View file

@ -126,15 +126,22 @@ class Menu extends Tag
return $xhtml; return $xhtml;
} }
$xhtml = self::tagHtml('li', $node->isActive() $xhtml = self::tagHtml(
'li',
$node->isActive()
? array('class' => $this->_activeClass) : null, ? array('class' => $this->_activeClass) : null,
false, false, true); false,
false,
true
);
// Generate the link. // Generate the link.
$xhtml .= self::linkTo($node->getHref(), $node->getCaption()); $xhtml .= self::linkTo($node->getHref(), $node->getCaption());
if ($node->isActive() && $node->hasChildren() if (
&& ($max_depth === null || $depth < $max_depth)) { $node->isActive() && $node->hasChildren()
&& ($max_depth === null || $depth < $max_depth)
) {
$xhtml .= $this->_renderMenu($node->getChildren(), $depth + 1, $max_depth); $xhtml .= $this->_renderMenu($node->getChildren(), $depth + 1, $max_depth);
} }

View file

@ -81,4 +81,3 @@ class Container
return $this; return $this;
} }
} }

View file

@ -16,6 +16,8 @@ use Phalcon\Di\FactoryDefault as DiDefault,
Phalcon\Cache\Frontend\Data as FrontendDataCache, Phalcon\Cache\Frontend\Data as FrontendDataCache,
Phalcon\Cache\Backend\Apc as BackendApcCache, Phalcon\Cache\Backend\Apc as BackendApcCache,
Phalcon\Translate\Adapter\NativeArray as TranslateAdapter, Phalcon\Translate\Adapter\NativeArray as TranslateAdapter,
Phalcon\Storage\AdapterFactory as StorageAdapterFactory,
Phalcon\Cache\AdapterFactory as CacheAdapterFactory,
Phalcon\Logger, Phalcon\Logger,
Phalcon\Mvc\Router; Phalcon\Mvc\Router;
@ -30,7 +32,8 @@ use Httpcb\Auth,
use App\Listener\AccessListener, use App\Listener\AccessListener,
App\Listener\DispatchListener, App\Listener\DispatchListener,
App\Listener\ActivityLog; App\Listener\ActivityLog,
App\Listener\AuthEmailListener;
class Services extends DiDefault class Services extends DiDefault
{ {
@ -46,8 +49,7 @@ class Services extends DiDefault
if (substr($method->getName(), 0, 11) == '_initShared') { if (substr($method->getName(), 0, 11) == '_initShared') {
$service = lcfirst(substr($method->getName(), 11)); $service = lcfirst(substr($method->getName(), 11));
$this->setShared($service, $method->getClosure($this)); $this->setShared($service, $method->getClosure($this));
} } else if (substr($method->getName(), 0, 5) == '_init') {
else if (substr($method->getName(),0, 5) == '_init') {
$service = lcfirst(substr($method->getName(), 5)); $service = lcfirst(substr($method->getName(), 5));
$this->set($service, $method->getClosure($this)); $this->set($service, $method->getClosure($this));
} }
@ -177,8 +179,11 @@ class Services extends DiDefault
$options = $mdConfig['options']; $options = $mdConfig['options'];
$adapter = $mdConfig['adapter']; $adapter = $mdConfig['adapter'];
$serializerFactory = new \Phalcon\Storage\SerializerFactory();
$factory = new CacheAdapterFactory($serializerFactory);
$class = 'Phalcon\Mvc\Model\MetaData\\' . $adapter; $class = 'Phalcon\Mvc\Model\MetaData\\' . $adapter;
return new $class($options); return new $class($factory, $options);
} }
// Otherwise, default to Memory. // Otherwise, default to Memory.
@ -195,8 +200,11 @@ class Services extends DiDefault
$adapter_name = isset($data['adapter']) ? $data['adapter'] : 'Stream'; $adapter_name = isset($data['adapter']) ? $data['adapter'] : 'Stream';
$options = $data['options']; $options = $data['options'];
$serializerFactory = new \Phalcon\Storage\SerializerFactory();
$factory = new StorageAdapterFactory($serializerFactory);
$class = 'Phalcon\Session\Adapter\\' . $adapter_name; $class = 'Phalcon\Session\Adapter\\' . $adapter_name;
$adapter = new $class($options); $adapter = new $class($factory, $options);
} }
// Default to Stream // Default to Stream
else { else {
@ -310,7 +318,7 @@ class Services extends DiDefault
$config = $this->get('config')->router; $config = $this->get('config')->router;
// Create the router // Create the router
$router = new Router(); $router = new Router(false);
$router->removeExtraSlashes($config->get('removeExtraSlashes', false)); $router->removeExtraSlashes($config->get('removeExtraSlashes', false));
foreach ($config->routes as $name => $def) { foreach ($config->routes as $name => $def) {
@ -327,6 +335,9 @@ class Services extends DiDefault
$router->add($def->get('pattern'), $path) $router->add($def->get('pattern'), $path)
->setName($name); ->setName($name);
} }
$router->notFound(['controller' => 'error', 'action' => 'show404']);
return $router; return $router;
} }
@ -337,6 +348,7 @@ class Services extends DiDefault
$eventsManager = new \Phalcon\Events\Manager(); $eventsManager = new \Phalcon\Events\Manager();
$eventsManager->attach('user', $activityLog); $eventsManager->attach('user', $activityLog);
$eventsManager->attach('auth', $activityLog); $eventsManager->attach('auth', $activityLog);
$eventsManager->attach('auth', new AuthEmailListener);
return $eventsManager; return $eventsManager;
} }

View file

@ -33,7 +33,8 @@ class ServerUrl extends AbstractHelper
// remove port if it's the default port. // remove port if it's the default port.
if (($scheme == 'http' && $port == 80) if (($scheme == 'http' && $port == 80)
|| ($scheme == 'https' && $port == 443)) { || ($scheme == 'https' && $port == 443)
) {
$port = null; $port = null;
} }

View file

@ -23,6 +23,19 @@ class ActivityLog extends Injectable
$this->_log($auth->getUser(), sprintf("Logged in (%s)", $type)); $this->_log($auth->getUser(), sprintf("Logged in (%s)", $type));
} }
/**
* On Impersonate event.
*
* @param Event $event
* @param Auth $auth
* @param User $user The user Impersonating the user in $auth
*/
public function onImpersonate(Event $event, Auth $auth, User $user)
{
$imp = $auth->getUser();
$this->_log($user, sprintf("Impersonated user (%s:%s)", $imp->getId(), $imp->getUsername()));
}
/** /**
* @param Event $event * @param Event $event
* @param User $auth * @param User $auth
@ -65,9 +78,21 @@ class ActivityLog extends Injectable
$this->_log($user, sprintf("OAuth disconnected (%s)", $providerName)); $this->_log($user, sprintf("OAuth disconnected (%s)", $providerName));
} }
/**
* Fired when a activation email is sent.
*
* @param Event $event
* @param User $user
* @param string $providerName
*/
public function onSentActivation(Event $event, User $user)
{
$this->_log($user, sprintf("Activation email sent"));
}
protected function _log(User $user, $message) protected function _log(User $user, $message)
{ {
$ip = (new \Phalcon\Http\Request())->getClientAddress(); $ip = (new \Phalcon\Http\Request())->getClientAddress(true);
return (new ActivityLogger())->assign([ return (new ActivityLogger())->assign([
'user_id' => $user->getId(), 'user_id' => $user->getId(),

View file

@ -0,0 +1,25 @@
<?php
namespace App\Listener;
use App\Model\Data\User,
App\Model\Data\UserActivation;
use Phalcon\Di\Injectable,
Phalcon\Events\Event;
class AuthEmailListener extends Injectable
{
public function onSentActivation(Event $event, User $user)
{
$activation = new UserActivation();
$activation->setUserId($user->getId())
->save();
$content = $this->di->getShared('template')->render('mail/account_activation', [
'link' => $activation->getActivationKey()
]);
$this->di->getMail()->send('Httpcb account activation', $user->getEmail(), $content);
}
}

View file

@ -10,8 +10,12 @@ class CreateRequestMetaTable extends AbstractMigration
$table = $this->table('request_meta'); $table = $this->table('request_meta');
$table->addColumn('callbackid', 'integer'); $table->addColumn('callbackid', 'integer');
$table->addForeignKey('callbackid', 'callback', [ 'id' ], $table->addForeignKey(
[ 'constraint' => 'FK_callback' ]); 'callbackid',
'callback',
['id'],
['constraint' => 'FK_callback']
);
$table->addColumn('source_ip', 'string', [ $table->addColumn('source_ip', 'string', [
'limit' => 50, 'limit' => 50,

View file

@ -9,8 +9,12 @@ class CreateRequestObjectTable extends AbstractMigration
{ {
$table = $this->table('request_object'); $table = $this->table('request_object');
$table->addForeignKey('id', 'request_meta', ['id'], $table->addForeignKey(
[ 'constraint' => 'FK_request_meta' ]); 'id',
'request_meta',
['id'],
['constraint' => 'FK_request_meta']
);
$table->addColumn('headers', 'blob', [ $table->addColumn('headers', 'blob', [
'null' => true, 'null' => true,

View file

@ -265,6 +265,16 @@ class User extends Base
return $this->status == self::STATUS_ACTIVE; return $this->status == self::STATUS_ACTIVE;
} }
/**
* Returns true if this user is suspended.
*
* @return bool
*/
public function isSuspended()
{
return $this->status == self::STATUS_SUSPENDED;
}
/** /**
* @return mixed * @return mixed
*/ */

View file

@ -44,4 +44,3 @@ abstract class Base implements ModuleDefinitionInterface
)); ));
} }
} }

View file

@ -12,6 +12,8 @@
data-bs-toggle="dropdown" role="button" aria-expanded="false"> data-bs-toggle="dropdown" role="button" aria-expanded="false">
{{ icon('solid/user') }} <strong>{{ auth.getUser().username }}</strong> {{ icon('solid/user') }} <strong>{{ auth.getUser().username }}</strong>
{% set imp = auth.getImpersonator() %}
{% if imp %}( {{ icon('solid/user-secret') }} {{ imp.username }} ){% endif %}
</a> </a>
<ul class="dropdown-menu navigation-user-menu-dropdown-list"> <ul class="dropdown-menu navigation-user-menu-dropdown-list">

View file

@ -56,6 +56,13 @@
{% if (user.getId()) %} {% if (user.getId()) %}
{% set actions = [ 'Activate': 'Active', 'Suspend': 'Suspended', 'Delete': 'Deleted' ] %} {% set actions = [ 'Activate': 'Active', 'Suspend': 'Suspended', 'Delete': 'Deleted' ] %}
<div class="float-end"> <div class="float-end">
{% if user.isSuspended() %}
<a class="button button-info" href="{{ url(['for': 'backend-user-activation-email', 'id': user.getId() ]) }}">
Send activation email
</a>
{% endif %}
{% for label, status in actions %} {% for label, status in actions %}
{% if (user.status != status) %} {% if (user.status != status) %}

View file

@ -20,6 +20,7 @@
<th>Email</th> <th>Email</th>
<th>Type</th> <th>Type</th>
<th>Status</th> <th>Status</th>
<th>&nbsp;</th>
</tr> </tr>
</thead> </thead>
@ -36,7 +37,12 @@
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td>{{ item.email }}</td> <td>{{ item.email }}</td>
<td>{{ item.type | capitalize }}</td> <td>{{ item.type | capitalize }}</td>
<td>{{ item.status }}</td> <td><span class="badge {{ item.isActive() ? 'badge-success' : 'badge-danger' }}">{{ item.status }}</span></td>
<td>
<a title="Impersonate" href="{{ url(['for': 'backend-user-impersonate', 'id': item.id ]) }}">
{{ icon('solid/user-secret') }}
</a>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View file

@ -7,6 +7,7 @@
"ext-psr": ">=1.2", "ext-psr": ">=1.2",
"ext-redis": "*", "ext-redis": "*",
"ext-yaml": "*", "ext-yaml": "*",
"ext-mbstring": "*",
"robmorgan/phinx": "^0.10.6", "robmorgan/phinx": "^0.10.6",
"league/oauth2-client": "^2.3", "league/oauth2-client": "^2.3",
"league/oauth2-github": "^2.0", "league/oauth2-github": "^2.0",

5
package-lock.json generated
View file

@ -1,11 +1,12 @@
{ {
"name": "httpcb", "name": "httpcb",
"version": "1.1", "version": "1.1.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"version": "1.1", "name": "httpcb",
"version": "1.1.0",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@popperjs/core": "^2.11.5", "@popperjs/core": "^2.11.5",

View file

@ -1,6 +1,6 @@
{ {
"name": "httpcb", "name": "httpcb",
"version": "1.1", "version": "1.1.0",
"description": "HTTP Callback Tool", "description": "HTTP Callback Tool",
"devDependencies": { "devDependencies": {
"@popperjs/core": "^2.11.5", "@popperjs/core": "^2.11.5",