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
@ -58,7 +59,7 @@ class ApiController extends ControllerBase
*/ */
public function activationLinkAction($id) public function activationLinkAction($id)
{ {
$link = UserActivation::findFirst(['activation_key = ?0', 'bind' => [ $id ]]); $link = UserActivation::findFirst(['activation_key = ?0', 'bind' => [$id]]);
if ($link) { if ($link) {
if ($link->isValid()) { if ($link->isValid()) {

View file

@ -33,8 +33,8 @@ class AuthController extends ControllerBase
} else { } else {
$msg = '<ul>'; $msg = '<ul>';
foreach($form->getMessages() as $message) { foreach ($form->getMessages() as $message) {
$msg .= '<li><strong>' . $message->getField() . '</strong> '. $message->getMessage() . '</li>'; $msg .= '<li><strong>' . $message->getField() . '</strong> ' . $message->getMessage() . '</li>';
} }
$msg .= '</ul>'; $msg .= '</ul>';
@ -47,7 +47,7 @@ class AuthController extends ControllerBase
public function oauthAction($provider_name) public function oauthAction($provider_name)
{ {
$client = $this->getDI()->get('oauth', [ $provider_name ]); $client = $this->getDI()->get('oauth', [$provider_name]);
$code = $this->request->get('code'); $code = $this->request->get('code');
$state = $this->request->get('state'); $state = $this->request->get('state');
@ -92,7 +92,7 @@ class AuthController extends ControllerBase
// User is logged in. // User is logged in.
$this->response->redirect('/'); $this->response->redirect('/');
} }
} catch(\Exception $e) { } catch (\Exception $e) {
throw $e; throw $e;
$this->flash->message('error', 'Failed to authenticate.'); $this->flash->message('error', 'Failed to authenticate.');
if ($this->auth->getUser()) { if ($this->auth->getUser()) {
@ -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,24 +55,23 @@ 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);
} }
} }
} else { } else {
$msg = '<ul>'; $msg = '<ul>';
foreach($form->getMessages() as $message) { foreach ($form->getMessages() as $message) {
$msg .= '<li><strong>' . $message->getField() . '</strong>: '. $message->getMessage() . '</li>'; $msg .= '<li><strong>' . $message->getField() . '</strong>: ' . $message->getMessage() . '</li>';
} }
$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 "
@ -137,7 +140,7 @@ class UserController extends ControllerBase
$msg = '<p>You are about to unlink the last OAuth provider.' $msg = '<p>You are about to unlink the last OAuth provider.'
. ' Your <strong>only</strong> login option will be <strong>password</strong> if you do this.</p>' . ' Your <strong>only</strong> login option will be <strong>password</strong> if you do this.</p>'
. '<p>Are you sure? <a class="alert-link" href="' . $url .'">Yes</a></p>'; . '<p>Are you sure? <a class="alert-link" href="' . $url . '">Yes</a></p>';
$this->flash->message('warning', $msg); $this->flash->message('warning', $msg);
$this->response->redirect('/settings'); $this->response->redirect('/settings');

View file

@ -17,7 +17,7 @@ class UserController extends \Phalcon\Mvc\Controller
*/ */
public function indexAction($page = 1) public function indexAction($page = 1)
{ {
$paginator = User::getPaginationList($page,15); $paginator = User::getPaginationList($page, 15);
$this->view->pagination_url = '/admin/user/list/'; $this->view->pagination_url = '/admin/user/list/';
$this->view->page = $paginator->paginate(); $this->view->page = $paginator->paginate();
@ -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',
]) ])
@ -63,7 +69,7 @@ class Registration extends FormBase
$this->add($username); $this->add($username);
// Names // Names
foreach([ 'first-name' => 'Firstname', 'last-name' => 'Lastname' ] as $id => $label) { foreach (['first-name' => 'Firstname', 'last-name' => 'Lastname'] as $id => $label) {
$name = new Text($id, array( $name = new Text($id, array(
'class' => 'form-control', 'class' => 'form-control',
@ -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,
@ -56,15 +60,15 @@ class UserSettings extends FormBase
// Id // Id
if ($entity && $entity->getId()) { if ($entity && $entity->getId()) {
$id = new Text('id', array( $id = new Text('id', array(
'class' => 'form-control', 'class' => 'form-control',
'readonly' => '', 'readonly' => '',
'disabled' => 'disabled', 'disabled' => 'disabled',
)); ));
$id->addValidator(new IdenticalValidator([ $id->addValidator(new IdenticalValidator([
'accepted' => $entity->getId(), 'accepted' => $entity->getId(),
'allowEmpty' => true 'allowEmpty' => true
])); ]));
$id->setLabel('ID'); $id->setLabel('ID');
@ -86,7 +90,7 @@ class UserSettings extends FormBase
); );
if ($entity && strlen($entity->getUsername())) { if ($entity && strlen($entity->getUsername())) {
$validator_options['except'] = [ $entity->getUsername() ]; $validator_options['except'] = [$entity->getUsername()];
} }
$username->addValidators([ $username->addValidators([
@ -116,12 +120,12 @@ class UserSettings extends FormBase
'class' => 'form-control', 'class' => 'form-control',
'placeholder' => 'Email', 'placeholder' => 'Email',
'readonly' => '', 'readonly' => '',
'disabled' => 'disabled', 'disabled' => 'disabled',
)); ));
$email->addValidator(new IdenticalValidator([ $email->addValidator(new IdenticalValidator([
'accepted' => $entity->getEmail(), 'accepted' => $entity->getEmail(),
'allowEmpty' => true 'allowEmpty' => true
])); ]));
} else { } else {
$email = new Text('email', array( $email = new Text('email', array(
@ -136,7 +140,7 @@ class UserSettings extends FormBase
]; ];
if ($entity && strlen($entity->getEmail())) { if ($entity && strlen($entity->getEmail())) {
$validator_options['except'] = [ $entity->getEmail() ]; $validator_options['except'] = [$entity->getEmail()];
} }
$email->addValidators([ $email->addValidators([
@ -191,7 +195,7 @@ class UserSettings extends FormBase
if ($this->_admin === false && $entity && strlen($entity->getPassword()) > 0) { if ($this->_admin === false && $entity && strlen($entity->getPassword()) > 0) {
$validation->add('passwordCurrent', new CallbackValidator([ $validation->add('passwordCurrent', new CallbackValidator([
'callback' => function($data) { 'callback' => function ($data) {
$new_pw = $data['passwordNew']; $new_pw = $data['passwordNew'];
if (strlen($new_pw) > 0) { if (strlen($new_pw) > 0) {
$value = $data['passwordCurrent']; $value = $data['passwordCurrent'];

View file

@ -3,7 +3,7 @@
namespace Httpcb; namespace Httpcb;
use Phalcon\Config, use Phalcon\Config,
Phalcon\Acl\Enum, Phalcon\Acl\Enum,
Phalcon\Acl\Role, Phalcon\Acl\Role,
Phalcon\Acl\Adapter\Memory as Adapter; Phalcon\Acl\Adapter\Memory as Adapter;
@ -40,7 +40,7 @@ class Acl
$pos = strpos($resource, '/'); $pos = strpos($resource, '/');
if ($pos !== false) { if ($pos !== false) {
// Construct the wildcard resource. // Construct the wildcard resource.
$wildcard = substr($resource, 0, $pos+1) . '*'; $wildcard = substr($resource, 0, $pos + 1) . '*';
// If we have this wildcard resource, check against that instead. // If we have this wildcard resource, check against that instead.
if ($this->hasResource($wildcard)) { if ($this->hasResource($wildcard)) {
@ -62,7 +62,7 @@ class Acl
public function fromConfig(Config $config) public function fromConfig(Config $config)
{ {
// Add roles. // Add roles.
foreach($config->roles as $name => $def) { foreach ($config->roles as $name => $def) {
$inherits = null; $inherits = null;
$description = null; $description = null;
@ -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);
@ -78,33 +77,33 @@ class Acl
} }
// Zones // Zones
foreach($config->zones as $name => $resources) { foreach ($config->zones as $name => $resources) {
if (!($resources instanceof Config)) { if (!($resources instanceof Config)) {
$resources = new Config([ $resources ]); $resources = new Config([$resources]);
} }
foreach($resources as $resource) { foreach ($resources as $resource) {
$this->_adapter->addComponent($resource, 'All'); $this->_adapter->addComponent($resource, 'All');
} }
} }
// Grant access for roles and resources. // Grant access for roles and resources.
foreach($config->roles as $name => $def) { foreach ($config->roles as $name => $def) {
$zones = $def->get('allowed-zones', []); $zones = $def->get('allowed-zones', []);
if (is_string($zones)) { if (is_string($zones)) {
$zones = [ $zones ]; $zones = [$zones];
} }
foreach($zones as $zone) { foreach ($zones as $zone) {
$resources = $config->zones->get($zone); $resources = $config->zones->get($zone);
if (!($resources instanceof Config)) { if (!($resources instanceof Config)) {
$resources = new Config([ $resources ]); $resources = new Config([$resources]);
} }
foreach($resources as $resource) { foreach ($resources as $resource) {
$this->_adapter->allow($name, $resource, 'All'); $this->_adapter->allow($name, $resource, 'All');
} }
} }
} }

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()
{ {
$this->session->remove(self::SESSION_KEY); $imp_id = $this->session->get(self::IMPERSONATOR_ID);
if ($imp_id !== null) {
$this->impersonateClear($imp_id);
} else {
$this->session->remove(self::SESSION_KEY);
}
return $this; return $this;
} }
} }

View file

@ -2,12 +2,13 @@
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)
{ {
// format the label // format the label
$label = ($label===null) ? '' : rtrim($label) . ' '; $label = ($label === null) ? '' : rtrim($label) . ' ';
// var_dump the variable into a buffer and keep the output // var_dump the variable into a buffer and keep the output
ob_start(); ob_start();
var_dump($var); var_dump($var);

View file

@ -11,7 +11,7 @@ class Form extends FormBase
{ {
$options = [ $options = [
'label-class' => 'col-form-label text-end', 'label-class' => 'col-form-label text-end',
'class' => [ 'col-sm-10' ], 'class' => ['col-sm-10'],
'message' => '' 'message' => ''
]; ];
@ -31,7 +31,7 @@ class Form extends FormBase
if ($len === 'full') { if ($len === 'full') {
$options['class'] = []; $options['class'] = [];
} else { } else {
$options['class'] = [ 'col-sm-' . $len ]; $options['class'] = ['col-sm-' . $len];
} }
unset($opt['length']); unset($opt['length']);
@ -42,10 +42,10 @@ class Form extends FormBase
protected function _render(AbstractElement $ele, $opt) protected function _render(AbstractElement $ele, $opt)
{ {
$classes = ['class' => 'form-control']; $classes = ['class' => 'form-control'];
if ($ele->hasMessages()) { if ($ele->hasMessages()) {
$classes['class'] .= ' is-invalid'; $classes['class'] .= ' is-invalid';
} }
$xhtml = ''; $xhtml = '';
@ -53,14 +53,17 @@ 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']) . '">'
. $ele->render($classes); . $ele->render($classes);
if ($ele->hasMessages()) { if ($ele->hasMessages()) {
$msg = $ele->getMessages()->current(); $msg = $ele->getMessages()->current();
$xhtml .= '<span class="invalid-feedback">' . $msg . '</span>'; $xhtml .= '<span class="invalid-feedback">' . $msg . '</span>';
} }

View file

@ -90,7 +90,7 @@ class Menu extends Tag
{ {
$xhtml = ''; $xhtml = '';
foreach($nodes as $node) { foreach ($nodes as $node) {
$xhtml .= $this->_renderNode($node, $depth, $max_depth); $xhtml .= $this->_renderNode($node, $depth, $max_depth);
} }
@ -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

@ -44,10 +44,10 @@ class RandomId extends Behavior implements BehaviorInterface
*/ */
public function notify($type, \Phalcon\Mvc\ModelInterface $model) public function notify($type, \Phalcon\Mvc\ModelInterface $model)
{ {
switch($type) { switch ($type) {
case 'beforeValidationOnCreate' : case 'beforeValidationOnCreate':
$this->generateId($model); $this->generateId($model);
break; break;
} }
} }
@ -69,7 +69,7 @@ class RandomId extends Behavior implements BehaviorInterface
if ($model->$field === null) { if ($model->$field === null) {
$random = new \Phalcon\Security\Random(); $random = new \Phalcon\Security\Random();
for($i = 0; $i < 3; $i++) { for ($i = 0; $i < 3; $i++) {
$id = substr($random->base64Safe(), 0, $len); $id = substr($random->base64Safe(), 0, $len);
$count = $model->count(array( $count = $model->count(array(

View file

@ -11,7 +11,7 @@ class Navigation extends Navigation\Container
*/ */
public function __construct($config) public function __construct($config)
{ {
foreach($config as $node) { foreach ($config as $node) {
$this->addChild($node); $this->addChild($node);
} }
} }

View file

@ -38,7 +38,7 @@ class Container
$node = new Node(); $node = new Node();
foreach($child as $k => $v) { foreach ($child as $k => $v) {
if ($k == 'children') { if ($k == 'children') {
continue; continue;
@ -49,7 +49,7 @@ class Container
if (isset($child['children'])) { if (isset($child['children'])) {
foreach($child['children'] as $c_data) { foreach ($child['children'] as $c_data) {
$node->addChild($c_data); $node->addChild($c_data);
} }
} }
@ -75,10 +75,9 @@ class Container
*/ */
public function addChildren($children) public function addChildren($children)
{ {
foreach($children as $child) { foreach ($children as $child) {
$this->addChild($child); $this->addChild($child);
} }
return $this; return $this;
} }
} }

View file

@ -225,7 +225,7 @@ class Node extends Container
} }
// first. Check children. // first. Check children.
foreach($this->getChildren() as $child) { foreach ($this->getChildren() as $child) {
if ($child->isActive() == true) { if ($child->isActive() == true) {
$this->setActive(true); $this->setActive(true);

View file

@ -61,7 +61,7 @@ class League implements AdapterInterface
try { try {
$reflection = new \ReflectionClass($this->_provider); $reflection = new \ReflectionClass($this->_provider);
return $reflection->getShortName(); return $reflection->getShortName();
} catch(\ReflectionException $ex) { } catch (\ReflectionException $ex) {
return ''; return '';
} }
} }

View file

@ -12,10 +12,12 @@ use Phalcon\Di\FactoryDefault as DiDefault,
Phalcon\Flash\Direct as FlashDirect, Phalcon\Flash\Direct as FlashDirect,
Phalcon\Mvc\Model\Metadata\Memory as MemoryMetaData, Phalcon\Mvc\Model\Metadata\Memory as MemoryMetaData,
Phalcon\Mvc\Model\MetaData\Apc as ApcMetaData, Phalcon\Mvc\Model\MetaData\Apc as ApcMetaData,
Phalcon\Mvc\ViewBaseInterface, Phalcon\Mvc\ViewBaseInterface,
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
{ {
@ -41,13 +44,12 @@ class Services extends DiDefault
$reflection = new \ReflectionObject($this); $reflection = new \ReflectionObject($this);
$methods = $reflection->getMethods(\ReflectionMethod::IS_PROTECTED); $methods = $reflection->getMethods(\ReflectionMethod::IS_PROTECTED);
foreach($methods as $method) { foreach ($methods as $method) {
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));
} }
@ -81,7 +83,7 @@ class Services extends DiDefault
try { try {
$tmp = new Config($basePath . $file); $tmp = new Config($basePath . $file);
$config->merge($tmp); $config->merge($tmp);
} catch(\Phalcon\Config\Exception $e) { } catch (\Phalcon\Config\Exception $e) {
// Sometime went wrong. Log here? // Sometime went wrong. Log here?
} }
} }
@ -115,7 +117,7 @@ class Services extends DiDefault
$eventsManager = new \Phalcon\Events\Manager(); $eventsManager = new \Phalcon\Events\Manager();
$eventsManager->attach("dispatch", function($event, $dispatcher) { $eventsManager->attach("dispatch", function ($event, $dispatcher) {
$actionName = lcfirst(\Phalcon\Text::camelize($dispatcher->getActionName(), '-_')); $actionName = lcfirst(\Phalcon\Text::camelize($dispatcher->getActionName(), '-_'));
$dispatcher->setActionName($actionName); $dispatcher->setActionName($actionName);
}); });
@ -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.
@ -188,22 +193,25 @@ class Services extends DiDefault
protected function _initSession() protected function _initSession()
{ {
$config = $this->get('config'); $config = $this->get('config');
$session = new \Phalcon\Session\Manager(); $session = new \Phalcon\Session\Manager();
if (isset($config->session)) { if (isset($config->session)) {
$data = $config->session->toArray(); $data = $config->session->toArray();
$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 {
$adapter = new \Phalcon\Session\Adapter\Stream(); $adapter = new \Phalcon\Session\Adapter\Stream();
} }
$session->setAdapter($adapter); $session->setAdapter($adapter);
// Start session. // Start session.
$session->start(); $session->start();
@ -219,7 +227,7 @@ class Services extends DiDefault
$view = new View(); $view = new View();
$view->setViewsDir([ $config->application->viewsDir ]); $view->setViewsDir([$config->application->viewsDir]);
$view->setLayoutsDir('_layouts/'); $view->setLayoutsDir('_layouts/');
$view->setPartialsDir('_partials/'); $view->setPartialsDir('_partials/');
@ -310,10 +318,10 @@ 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) {
if (!($def instanceof \Phalcon\Config)) { if (!($def instanceof \Phalcon\Config)) {
continue; continue;
@ -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;
} }
@ -353,10 +365,10 @@ class Services extends DiDefault
protected function _initSharedLogger() protected function _initSharedLogger()
{ {
$path = $this->get('config')->application->logDir; $path = $this->get('config')->application->logDir;
return new \Phalcon\Logger('default', [ return new \Phalcon\Logger('default', [
'main' => new \Phalcon\Logger\Adapter\Stream($path . 'app.txt') 'main' => new \Phalcon\Logger\Adapter\Stream($path . 'app.txt')
]); ]);
} }
protected function _initTemplate() protected function _initTemplate()
@ -366,7 +378,7 @@ class Services extends DiDefault
$view = new SimpleView(); $view = new SimpleView();
$view->setViewsDir($config->application->templateDir); $view->setViewsDir($config->application->templateDir);
$view->registerEngines([ $view->registerEngines([
'.volt' => function (ViewBaseInterface $view) use ($config) { '.volt' => function (ViewBaseInterface $view) use ($config) {
$volt = new VoltEngine($view, $this); $volt = new VoltEngine($view, $this);
$volt->setOptions(array( $volt->setOptions(array(

View file

@ -21,7 +21,7 @@ class Alpha extends AbstractValidator
* @param string $field * @param string $field
* @return bool * @return bool
*/ */
public function validate(\Phalcon\Validation $validation, $field) : bool public function validate(\Phalcon\Validation $validation, $field): bool
{ {
$allowSpace = $this->getOption('allowSpace', false); $allowSpace = $this->getOption('allowSpace', false);
@ -44,7 +44,7 @@ class Alpha extends AbstractValidator
$message = $validation->getDefaultMessage('Alpha'); $message = $validation->getDefaultMessage('Alpha');
} }
$replace = [ ":field" => $label ]; $replace = [":field" => $label];
$code = $this->getOption("code"); $code = $this->getOption("code");
if (is_array($code)) { if (is_array($code)) {

View file

@ -14,7 +14,7 @@ abstract class AbstractHelper implements InjectionAwareInterface
* *
* @param DiInterface $container * @param DiInterface $container
*/ */
public function setDI(DiInterface $container) : void public function setDI(DiInterface $container): void
{ {
$this->_di = $container; $this->_di = $container;
} }
@ -24,7 +24,7 @@ abstract class AbstractHelper implements InjectionAwareInterface
* *
* @return DiInterface * @return DiInterface
*/ */
public function getDI() : DiInterface public function getDI(): DiInterface
{ {
return $this->_di; return $this->_di;
} }

View file

@ -27,7 +27,7 @@ class Icon extends AbstractHelper
if (is_array($args)) { if (is_array($args)) {
foreach($args as $arg) { foreach ($args as $arg) {
$classes[] .= 'fa-' . $arg; $classes[] .= 'fa-' . $arg;
} }
} }

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

@ -16,7 +16,7 @@ class Service implements InjectionAwareInterface
* *
* @param DiInterface $container * @param DiInterface $container
*/ */
public function setDI(DiInterface $container) : void public function setDI(DiInterface $container): void
{ {
$this->_di = $container; $this->_di = $container;
} }
@ -26,7 +26,7 @@ class Service implements InjectionAwareInterface
* *
* @return DiInterface * @return DiInterface
*/ */
public function getDI() : DiInterface public function getDI(): DiInterface
{ {
return $this->_di; return $this->_di;
} }

View file

@ -3,7 +3,7 @@
namespace App\Listener; namespace App\Listener;
use Phalcon\Di\Injectable, use Phalcon\Di\Injectable,
Phalcon\Events\Event, Phalcon\Events\Event,
Phalcon\Mvc\Dispatcher, Phalcon\Mvc\Dispatcher,
Phalcon\Mvc\Dispatcher\Exception as DispatcherException; Phalcon\Mvc\Dispatcher\Exception as DispatcherException;
@ -22,7 +22,7 @@ class AccessListener extends Injectable
* @return bool * @return bool
* @throws DispatcherException * @throws DispatcherException
*/ */
public function beforeExecuteRoute(Event $event, Dispatcher $dispatcher) : bool public function beforeExecuteRoute(Event $event, Dispatcher $dispatcher): bool
{ {
// If we have an identity, fetch type from authed user. // If we have an identity, fetch type from authed user.
if ($this->auth->hasIdentity()) { if ($this->auth->hasIdentity()) {

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,11 +78,23 @@ 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(),
'ip' => $ip, 'ip' => $ip,
'message' => $message 'message' => $message

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

@ -4,7 +4,7 @@ namespace App\Listener;
use Exception, use Exception,
Phalcon\Events\Event, Phalcon\Events\Event,
Phalcon\Di\Injectable, Phalcon\Di\Injectable,
Phalcon\Mvc\Dispatcher, Phalcon\Mvc\Dispatcher,
Phalcon\Mvc\Dispatcher\Exception as DispatcherException; Phalcon\Mvc\Dispatcher\Exception as DispatcherException;
@ -38,11 +38,11 @@ class DispatchListener extends Injectable
// was that an controller or action was not found. // was that an controller or action was not found.
if ($exception instanceof DispatcherException) { if ($exception instanceof DispatcherException) {
switch ($exception->getCode()) { switch ($exception->getCode()) {
case Dispatcher::EXCEPTION_HANDLER_NOT_FOUND : case Dispatcher::EXCEPTION_HANDLER_NOT_FOUND:
case Dispatcher::EXCEPTION_ACTION_NOT_FOUND : case Dispatcher::EXCEPTION_ACTION_NOT_FOUND:
// in this case, forward to 404 page. // in this case, forward to 404 page.
$dispatcher->forward($this->_route_notfound); $dispatcher->forward($this->_route_notfound);
return false; return false;
} }
} }

View file

@ -21,7 +21,7 @@ class CreateUserTable extends AbstractMigration
$table->addColumn('status', 'enum', [ $table->addColumn('status', 'enum', [
'null' => false, 'null' => false,
'default' => 'Active', 'default' => 'Active',
'values' => [ 'Active', 'Deleted', 'Suspended'] 'values' => ['Active', 'Deleted', 'Suspended']
]); ]);
$table->addColumn('password', 'string', [ $table->addColumn('password', 'string', [

View file

@ -11,11 +11,11 @@ class CreateCallbackTable extends AbstractMigration
$table->addColumn('public_id', 'string', [ $table->addColumn('public_id', 'string', [
'length' => 12, 'length' => 12,
'null' => false, 'null' => false,
])->addIndex('public_id', [ 'name' => 'UNIQUE_public_id', 'unique' => true ]); ])->addIndex('public_id', ['name' => 'UNIQUE_public_id', 'unique' => true]);
$table->addColumn('userid', 'integer', [ $table->addColumn('userid', 'integer', [
'null' => true, 'null' => true,
])->addForeignKey('userid', 'user', ['id'], [ 'constraint' => 'FK_user' ]); ])->addForeignKey('userid', 'user', ['id'], ['constraint' => 'FK_user']);
$table->addColumn('name', 'string', [ $table->addColumn('name', 'string', [
'length' => 64, 'length' => 64,

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,
@ -21,7 +25,7 @@ class CreateRequestMetaTable extends AbstractMigration
$table->addColumn('method', 'enum', [ $table->addColumn('method', 'enum', [
'null' => false, 'null' => false,
'default' => 'GET', 'default' => 'GET',
'values' => [ 'GET', 'POST' ] 'values' => ['GET', 'POST']
]); ]);
$table->addColumn('uri', 'string', [ $table->addColumn('uri', 'string', [

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

@ -8,11 +8,11 @@ class PasswordLink extends AbstractMigration
public function up() public function up()
{ {
$this->table('password_link') $this->table('password_link')
->addColumn('public_id', 'string', ['length' => 12 ]) ->addColumn('public_id', 'string', ['length' => 12])
->addColumn('user_id', 'integer') ->addColumn('user_id', 'integer')
->addForeignKey('user_id', 'user', ['id'], [ 'constraint' => 'FK_password_link_user' ]) ->addForeignKey('user_id', 'user', ['id'], ['constraint' => 'FK_password_link_user'])
->addColumn('date', 'datetime', [ 'default' => 'CURRENT_TIMESTAMP' ]) ->addColumn('date', 'datetime', ['default' => 'CURRENT_TIMESTAMP'])
->addColumn('password', 'string', [ 'limit' => 255, 'null' => true ]) ->addColumn('password', 'string', ['limit' => 255, 'null' => true])
->save(); ->save();
} }
} }

View file

@ -12,7 +12,7 @@ class UserSplitName extends AbstractMigration
// Rename "name" to "firstname" and add "lastname". // Rename "name" to "firstname" and add "lastname".
$this->table('user') $this->table('user')
->renameColumn('name', 'firstname') ->renameColumn('name', 'firstname')
->addColumn('lastname','string', [ ->addColumn('lastname', 'string', [
'length' => 128, 'length' => 128,
'after' => 'firstname', 'after' => 'firstname',
'null' => true 'null' => true
@ -20,7 +20,7 @@ class UserSplitName extends AbstractMigration
->save(); ->save();
// Update row data, moving everything after first space to from lastname. // Update row data, moving everything after first space to from lastname.
foreach($this->fetchAll("SELECT `id`,`firstname` FROM `user`") as $row) { foreach ($this->fetchAll("SELECT `id`,`firstname` FROM `user`") as $row) {
$builder = $this->getQueryBuilder()->update('user') $builder = $this->getQueryBuilder()->update('user')
->where(['id' => $row['id']]); ->where(['id' => $row['id']]);
@ -31,7 +31,7 @@ class UserSplitName extends AbstractMigration
$pos = strpos($firstname, ' '); $pos = strpos($firstname, ' ');
if ($pos !== false) { if ($pos !== false) {
// Set everything after the first space to lastname. // Set everything after the first space to lastname.
$builder->set('lastname', substr($firstname, $pos+1)); $builder->set('lastname', substr($firstname, $pos + 1));
// Remove everything after first space from firstname. // Remove everything after first space from firstname.
$firstname = substr($firstname, 0, $pos); $firstname = substr($firstname, 0, $pos);

View file

@ -11,7 +11,7 @@ class UserType extends AbstractMigration
->addColumn('type', 'enum', [ ->addColumn('type', 'enum', [
'null' => false, 'null' => false,
'default' => 'user', 'default' => 'user',
'values' => [ 'user', 'admin' ], 'values' => ['user', 'admin'],
'after' => 'regdate' 'after' => 'regdate'
]) ])
->save(); ->save();

View file

@ -18,7 +18,7 @@ class Base extends Model
* @param boolean $allFields * @param boolean $allFields
* @return bool * @return bool
*/ */
public function hasChanged($fieldName = null, bool $allFields = false) : bool public function hasChanged($fieldName = null, bool $allFields = false): bool
{ {
return $this->hasSnapshotData() === false return $this->hasSnapshotData() === false
|| parent::hasChanged($fieldName, $allFields); || parent::hasChanged($fieldName, $allFields);

View file

@ -216,8 +216,8 @@ class Callback extends Model
*/ */
public function initialize() public function initialize()
{ {
// Set table name mapped in the model. // Set table name mapped in the model.
$this->setSource('callback'); $this->setSource('callback');
$this->useDynamicUpdate(true); $this->useDynamicUpdate(true);

View file

@ -53,7 +53,7 @@ class Request extends Model
*/ */
public function setHeaders($headers) public function setHeaders($headers)
{ {
foreach($headers as $k => $v) { foreach ($headers as $k => $v) {
if (strlen($v) < 1) { if (strlen($v) < 1) {
unset($headers[$k]); unset($headers[$k]);

View file

@ -161,7 +161,7 @@ class RequestMeta extends Model
$query = (string) parse_url($this->getUri(), PHP_URL_QUERY); $query = (string) parse_url($this->getUri(), PHP_URL_QUERY);
$ret = array(); $ret = array();
foreach(explode('&', $query) as $v) { foreach (explode('&', $query) as $v) {
@list($k, $v) = explode('=', $v, 2); @list($k, $v) = explode('=', $v, 2);
if (strlen($k) > 0) { if (strlen($k) > 0) {
$ret[$k] = $v; $ret[$k] = $v;
@ -211,7 +211,7 @@ class RequestMeta extends Model
{ {
$headers = $this->getHeaders(); $headers = $this->getHeaders();
foreach($headers as $k => $v) { foreach ($headers as $k => $v) {
if ($k == 'Content-Length') { if ($k == 'Content-Length') {
return $v; return $v;
@ -224,7 +224,7 @@ class RequestMeta extends Model
{ {
$headers = $this->getHeaders(); $headers = $this->getHeaders();
foreach($headers as $k => $v) { foreach ($headers as $k => $v) {
if ($k == 'Content-Type') { if ($k == 'Content-Type') {
return substr($v, strrpos($v, '/') + 1); return substr($v, strrpos($v, '/') + 1);

View file

@ -61,7 +61,7 @@ class User extends Base
'message' => 'The username already exists.' 'message' => 'The username already exists.'
]), ]),
'email' => new CallbackValidator([ 'email' => new CallbackValidator([
'callback' => function() { 'callback' => function () {
return $this->findFirstByEmail($this->getEmail()) === false; return $this->findFirstByEmail($this->getEmail()) === false;
}, },
'message' => 'The email address already exists.' 'message' => 'The email address already exists.'
@ -69,7 +69,7 @@ class User extends Base
]; ];
$validation = new Validation(); $validation = new Validation();
foreach($rules as $field => $validator) { foreach ($rules as $field => $validator) {
// Only validate changed fields. // Only validate changed fields.
if ($this->hasChanged($field)) { if ($this->hasChanged($field)) {
@ -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
*/ */
@ -282,7 +292,7 @@ class User extends Base
$this->password = $password; $this->password = $password;
return $this; return $this;
} }
/** /**
* @return mixed * @return mixed
*/ */
@ -404,7 +414,7 @@ class User extends Base
{ {
return self::findFirst([ return self::findFirst([
"(email = :v: OR username = :v:) AND status != :s:", "(email = :v: OR username = :v:) AND status != :s:",
"bind" => [ 'v' => $value, 's' => self::STATUS_DELETED ] "bind" => ['v' => $value, 's' => self::STATUS_DELETED]
]); ]);
} }
@ -412,7 +422,7 @@ class User extends Base
{ {
return self::findFirst([ return self::findFirst([
"email = :email: AND status != :s:", "email = :email: AND status != :s:",
"bind" => [ 'email' => $email, 's' => self::STATUS_DELETED ] "bind" => ['email' => $email, 's' => self::STATUS_DELETED]
]); ]);
} }
@ -420,7 +430,7 @@ class User extends Base
{ {
return self::findFirst([ return self::findFirst([
"username = :username: AND status != :s:", "username = :username: AND status != :s:",
"bind" => [ 'username' => $username, 's' => self::STATUS_DELETED ] "bind" => ['username' => $username, 's' => self::STATUS_DELETED]
]); ]);
} }
@ -430,7 +440,7 @@ class User extends Base
return self::findFirst([ return self::findFirst([
"{$column}_id = :id: AND status != :s:", "{$column}_id = :id: AND status != :s:",
"bind" => [ 'id' => $oauth->getId(), 's' => self::STATUS_DELETED ] "bind" => ['id' => $oauth->getId(), 's' => self::STATUS_DELETED]
]); ]);
} }

View file

@ -79,7 +79,7 @@ class UserActivation extends Base
* @param int $id * @param int $id
* @return PasswordLink * @return PasswordLink
*/ */
public function setId(int $id) : UserActivation public function setId(int $id): UserActivation
{ {
$this->id = $id; $this->id = $id;
return $this; return $this;
@ -97,7 +97,7 @@ class UserActivation extends Base
* @param string $public_id * @param string $public_id
* @return UserActivation * @return UserActivation
*/ */
public function setActivationKey(string $key) : UserActivation public function setActivationKey(string $key): UserActivation
{ {
$this->activation_key = $key; $this->activation_key = $key;
return $this; return $this;
@ -115,7 +115,7 @@ class UserActivation extends Base
* @param int $user_id * @param int $user_id
* @return UserActivation * @return UserActivation
*/ */
public function setUserId(int $user_id) : UserActivation public function setUserId(int $user_id): UserActivation
{ {
$this->user_id = $user_id; $this->user_id = $user_id;
return $this; return $this;
@ -133,7 +133,7 @@ class UserActivation extends Base
* @param string $password * @param string $password
* @return UserActivation * @return UserActivation
*/ */
public function setPassword(string $password) : UserActivation public function setPassword(string $password): UserActivation
{ {
$this->password = $password; $this->password = $password;
return $this; return $this;
@ -144,7 +144,7 @@ class UserActivation extends Base
* *
* @return bool * @return bool
*/ */
public function isUsed() : bool public function isUsed(): bool
{ {
return (bool) $this->used; return (bool) $this->used;
} }
@ -154,7 +154,7 @@ class UserActivation extends Base
* *
* @return bool * @return bool
*/ */
public function isValid() : bool public function isValid(): bool
{ {
// Used links are not valid. // Used links are not valid.
if ($this->isUsed()) { if ($this->isUsed()) {
@ -175,9 +175,9 @@ class UserActivation extends Base
public function beforeCreate() public function beforeCreate()
{ {
// Creating a new link automatically removes old ones. // Creating a new link automatically removes old ones.
$links = self::find(["user_id = ?0", 'bind' => [ $this->user_id ]]); $links = self::find(["user_id = ?0", 'bind' => [$this->user_id]]);
foreach($links as $link) { foreach ($links as $link) {
$link->delete(); $link->delete();
} }
} }

View file

@ -39,9 +39,8 @@ abstract class Base implements ModuleDefinitionInterface
$dispatcher->setDefaultNamespace($this->_controllerNamespace); $dispatcher->setDefaultNamespace($this->_controllerNamespace);
$di->get('view')->setViewsDir(array_merge( $di->get('view')->setViewsDir(array_merge(
[ $this->_viewDir ], [$this->_viewDir],
(array) $di->get('view')->getViewsDir() (array) $di->get('view')->getViewsDir()
)); ));
} }
} }

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",