diff --git a/app/assets/sass/application.scss b/app/assets/sass/application.scss index a1f7d81..f44df01 100644 --- a/app/assets/sass/application.scss +++ b/app/assets/sass/application.scss @@ -44,3 +44,4 @@ @import "views/about"; @import "views/login"; @import "views/register"; +@import "views/backend"; diff --git a/app/assets/sass/views/_backend.scss b/app/assets/sass/views/_backend.scss new file mode 100644 index 0000000..519fd47 --- /dev/null +++ b/app/assets/sass/views/_backend.scss @@ -0,0 +1,33 @@ + +.backend { + display: flex; + @extend .section; + padding: 0; + + &-sidemenu { + @extend .list-group; + width: 15%; + background-color: darken($section-bg, 4%); + border: 1px solid darken($section-bg, 8%); + > li { + list-style: none; + + a { + display: block; + color: $text-secondary-color; + padding: .8em 1.6em; + + &:hover { + background: darken($section-bg, 8%); + color: $text-color; + text-decoration: none; + } + } + } + } + + &-content { + width: 85%; + padding: $section-padding; + } +} diff --git a/app/config/acl.yml b/app/config/acl.yml index 0f27795..738bcfc 100644 --- a/app/config/acl.yml +++ b/app/config/acl.yml @@ -1,28 +1,4 @@ -# ACL in this system is defined as follows: -# -# - Roles: -# Roles define a group of user. like Author, Admin, Guest etc. -# Each role can inherit other roles with the "inherit" key. -# Each role can gain access to a zone (explained later) by the -# "allowed-zones" key. Per default a role is denied access to all zones. -# -# - Resources: -# Resources maps directly to controller names. -# There a 2 controllers/resources that are a bit special, -# index and error resources are always accessible by everyone (e.g. they -# are not part of the ACL). -# -# - Access levels. -# These are not used in this system. a hardcoded "All" level is used. -# -# Zones -# -# Zones defines a group of resources. for example an "backend" zone can -# have 2 controllers/resources (site-config, user-manager) -# -# Zones might be implemented using modules later. - acl: roles: guest: @@ -32,11 +8,12 @@ acl: inherits: guest allowed-zones: user description: Logged in users - #admin: - # inherits: user - # allowed-zones: backend + admin: + inherits: user + description: Administrators + allowed-zones: backend zones: public: [ auth, api ] user: [ user, callback ] - #backend: [ site, user-man ] + backend: backend/* diff --git a/app/config/app.yml b/app/config/app.yml index 398b1cf..a486196 100644 --- a/app/config/app.yml +++ b/app/config/app.yml @@ -1,9 +1,10 @@ application: + modulesDir : ../app/modules/ controllersDir : ../app/controllers/ modelsDir : ../app/models/ migrationsDir : ../app/migrations/ - viewsDir : ../app/views/ + viewsDir : ../app/views/_common/ templateDir : ../app/templates/ listenersDir : ../app/listeners/ libraryDir : ../app/library/ diff --git a/app/config/menu.yml b/app/config/menu.yml index 5aa188e..65d24ec 100644 --- a/app/config/menu.yml +++ b/app/config/menu.yml @@ -25,3 +25,10 @@ menu: route: about-route controller: index action: about + admin: + caption: Admin + resource: backend/user + controller: user + action: index + route: backend-home + diff --git a/app/config/routes.yml b/app/config/routes.yml index 7b0f625..5510b37 100644 --- a/app/config/routes.yml +++ b/app/config/routes.yml @@ -56,3 +56,14 @@ router: path: controller: api action: activationlink + + # Backend + backend-home: + pattern: '/admin' + path: backend::user::index + backend-user-list: + pattern: '/admin/user/list/{page:([0-9]+)}' + path: backend::user::index + backend-log: + pattern: '/admin/log{page:/?([0-9]+)?}' + path: backend::log::index diff --git a/app/controllers/backend/LogController.php b/app/controllers/backend/LogController.php new file mode 100644 index 0000000..dcbe8fb --- /dev/null +++ b/app/controllers/backend/LogController.php @@ -0,0 +1,21 @@ +view->setLayout('side-menu'); + } + + public function indexAction($page = 1) + { + $paginator = ActivityLog::getAllPaginationList($page); + + $this->view->page = $paginator->getPaginate(); + $this->view->pagination_url = '/admin/log/'; + } +} diff --git a/app/controllers/backend/UserController.php b/app/controllers/backend/UserController.php new file mode 100644 index 0000000..bde79ac --- /dev/null +++ b/app/controllers/backend/UserController.php @@ -0,0 +1,24 @@ +view->setLayout('side-menu'); + } + + /** + * @param $page + */ + public function indexAction($page = 1) + { + $paginator = User::getPaginationList($page,15); + + $this->view->pagination_url = '/admin/user/list/'; + $this->view->page = $paginator->getPaginate(); + } +} diff --git a/app/library/Acl.php b/app/library/Acl.php index 80a3157..f00ac8c 100644 --- a/app/library/Acl.php +++ b/app/library/Acl.php @@ -33,6 +33,19 @@ class Acl */ public function isAllowed($role, $resource) { + // Special stuff here :) for resources within modules. + + // Modules and controllers are separated by "/" + $pos = strpos($resource, '/'); + if ($pos !== false) { + // Construct the wildcard resource. + $wildcard = substr($resource, 0, $pos+1) . '*'; + + // If we have this wildcard resource, check against that instead. + if ($this->hasResource($wildcard)) { + $resource = $wildcard; + } + } return $this->_adapter->isAllowed($role, $resource, 'All') == \Phalcon\Acl::ALLOW; } @@ -85,7 +98,8 @@ class Acl } foreach($zones as $zone) { - foreach($config->zones->get($zone) as $resource) { + $resources = (array) $config->zones->get($zone); + foreach($resources as $resource) { $this->_adapter->allow($name, $resource, 'All'); } } diff --git a/app/library/Bootstrap.php b/app/library/Bootstrap.php index 932ec34..d1eaa63 100644 --- a/app/library/Bootstrap.php +++ b/app/library/Bootstrap.php @@ -9,12 +9,19 @@ use Phalcon\Mvc\Application; class Bootstrap extends Injectable { + /** + * @var Application + */ + protected $_app; + public function __construct(DiInterface $di = null) { if ($di === null) { $di = new DiDefault(); } $this->setDI($di); + + $this->_app = new Application(); } /** @@ -34,6 +41,14 @@ class Bootstrap extends Injectable $di->get('debugger')->listen(true, true); } + // Modules + $this->_app->registerModules([ + 'main' => [ 'className' => 'App\Module\Main' ], + 'backend' => [ 'className' => 'App\Module\Backend' ], + ]); + + $this->_app->setDefaultModule('main'); + return $this; } @@ -44,7 +59,7 @@ class Bootstrap extends Injectable */ public function run() { - $app = new Application($this->getDI()); - return $app->handle()->getContent(); + $this->_app->setDI($this->getDI()); + return $this->_app->handle()->getContent(); } } diff --git a/app/library/Navigation/Node.php b/app/library/Navigation/Node.php index 5f9d420..fb1f6b6 100644 --- a/app/library/Navigation/Node.php +++ b/app/library/Navigation/Node.php @@ -183,11 +183,11 @@ class Node extends Container // Assemble route if set. if (strlen($this->getRoute()) > 0) { - $href = array( + $href = [ 'for' => $this->getRoute(), 'controller' => $this->getController(), 'action' => $this->getAction() - ); + ]; } // Otherwise, use default route. else { diff --git a/app/library/Services.php b/app/library/Services.php index af06def..a6157f5 100644 --- a/app/library/Services.php +++ b/app/library/Services.php @@ -99,6 +99,7 @@ class Services extends DiDefault $loader->registerNamespaces(array( 'App\Controller' => $config->application->controllersDir, 'App\Listener' => $config->application->listenersDir, + 'App\Module' => $config->application->modulesDir, 'App\Model' => $config->application->modelsDir, 'App\Form' => $config->application->formsDir, 'Httpcb' => $config->application->libraryDir, @@ -222,7 +223,7 @@ class Services extends DiDefault $view = new View(); - $view->setViewsDir($config->application->viewsDir); + $view->setViewsDir([ $config->application->viewsDir ]); $view->setLayoutsDir('_layouts/'); $view->setPartialsDir('_partials/'); @@ -288,7 +289,8 @@ class Services extends DiDefault $menu = new Menu($navigation); $menu->setMenuClass(null); if ($this->get('auth')->hasIdentity()) { - $menu->setAclRole(Acl::ROLE_USER); + $type = $this->get('auth')->getIdentity()->getType(); + $menu->setAclRole($type); } else { $menu->setAclRole(Acl::ROLE_GUEST); } diff --git a/app/listeners/AccessListener.php b/app/listeners/AccessListener.php index 92468e0..e51301c 100644 --- a/app/listeners/AccessListener.php +++ b/app/listeners/AccessListener.php @@ -24,15 +24,17 @@ class AccessListener extends Plugin */ public function beforeExecuteRoute(Event $event, Dispatcher $dispatcher) : bool { - // We only have two roles for now, authenticated users and guests. + // If we have an identity, fetch type from authed user. if ($this->auth->hasIdentity()) { - $role = Acl::ROLE_USER; - } else { + $user = $this->auth->getUser(); + $role = $user->getType(); + } + // Othersize, we default to role. + else { $role = Acl::ROLE_GUEST; } - // Get the resource from controller name. - $resource = $dispatcher->getControllerName(); + $resource = $this->_getCurrentResource($dispatcher); // Ignore checks for error resource. if (in_array($resource, $this->_ignored_resources)) { @@ -67,4 +69,15 @@ class AccessListener extends Plugin } return true; } + + protected function _getCurrentResource($dispatcher) + { + // If default module, only fetch controller name. + if (strlen($dispatcher->getModuleName()) < 1) { + return $dispatcher->getControllerName(); + } + + // Otherwise, we follow the syntax "/" + return "{$dispatcher->getModuleName()}/{$dispatcher->getControllerName()}"; + } } diff --git a/app/migrations/20181003134001_user_type.php b/app/migrations/20181003134001_user_type.php new file mode 100644 index 0000000..20f02b1 --- /dev/null +++ b/app/migrations/20181003134001_user_type.php @@ -0,0 +1,19 @@ +table('user') + ->addColumn('type', 'enum', [ + 'null' => false, + 'default' => 'user', + 'values' => [ 'user', 'admin' ], + 'after' => 'regdate' + ]) + ->save(); + } +} diff --git a/app/models/Data/ActivityLog.php b/app/models/Data/ActivityLog.php index 91f3158..1bf3c9b 100644 --- a/app/models/Data/ActivityLog.php +++ b/app/models/Data/ActivityLog.php @@ -2,7 +2,8 @@ namespace App\Model\Data; -use \Phalcon\Paginator\Adapter\QueryBuilder; +use Phalcon\Mvc\Model\Query\BuilderInterface, + Phalcon\Paginator\Adapter\QueryBuilder; class ActivityLog extends Base { @@ -16,6 +17,15 @@ class ActivityLog extends Base protected $message; + /** + * Initialize method for model. + */ + public function initialize() + { + // Relationships + $this->hasOne('user_id', User::class, 'id', ['alias' => 'User']); + } + /** * @return mixed */ @@ -120,6 +130,26 @@ class ActivityLog extends Base ->where('user_id = :uid:', array('uid' => $userid)) ->orderBy('timestamp DESC'); + return self::_paginate($builder, $page, $limit); + } + + /** + * @param int $page + * @param int $limit + * @return \Phalcon\Paginator\AdapterInterface + */ + public static function getAllPaginationList($page = 1, $limit = 30) + { + $builder = (new self())->getModelsManager()->createBuilder(); + + $builder->from(self::class) + ->orderBy('timestamp DESC'); + + return self::_paginate($builder, $page, $limit); + } + + protected static function _paginate(BuilderInterface $builder, $page = 1, $limit = 30) + { $paginator = new QueryBuilder(array( 'builder' => $builder, 'page' => $page, diff --git a/app/models/Data/User.php b/app/models/Data/User.php index 335a874..122ba68 100644 --- a/app/models/Data/User.php +++ b/app/models/Data/User.php @@ -9,6 +9,9 @@ use Phalcon\Validation, class User extends Base { + const TYPE_USER = 'user'; + const TYPE_ADMIN = 'admin'; + const STATUS_ACTIVE = 'Active'; const STATUS_DELETED = 'Deleted'; const STATUS_SUSPENDED = 'Suspended'; @@ -23,6 +26,8 @@ class User extends Base protected $email; + protected $type; + protected $status; protected $password; @@ -203,6 +208,24 @@ class User extends Base return $this; } + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * @param string $type + * @return User + */ + public function setType($type) + { + $this->type = $type; + return $this; + } + /** * @return string */ @@ -432,4 +455,24 @@ class User extends Base } } } + + /** + * @param int $page + * @param int $limit + * @return \Phalcon\Paginator\AdapterInterface + */ + public static function getPaginationList($page = 1, $limit = 30) + { + $builder = (new self())->getModelsManager()->createBuilder(); + + $builder->from(self::class); + + $paginator = new \Phalcon\Paginator\Adapter\QueryBuilder(array( + 'builder' => $builder, + 'page' => $page, + 'limit' => $limit + )); + + return $paginator; + } } diff --git a/app/modules/Backend.php b/app/modules/Backend.php new file mode 100644 index 0000000..aa1ca6e --- /dev/null +++ b/app/modules/Backend.php @@ -0,0 +1,12 @@ +registerNamespaces([ + $this->_controllerNamespace => $this->_controllerPath, + ]); + + $loader->register(); + } + + /** + * Register specific services for the module + */ + public function registerServices(DiInterface $di) + { + $dispatcher = $di->get('dispatcher'); + + $dispatcher->setDefaultNamespace($this->_controllerNamespace); + + $di->get('view')->setViewsDir(array_merge( + [ $this->_viewDir ], + (array) $di->get('view')->getViewsDir() + )); + } +} + diff --git a/app/modules/Main.php b/app/modules/Main.php new file mode 100644 index 0000000..5e397b0 --- /dev/null +++ b/app/modules/Main.php @@ -0,0 +1,12 @@ +
- {% include "_templates/navigation.volt" %} + {% include "_components/navigation.volt" %}
{% block masthead %}{% endblock %} @@ -20,14 +20,14 @@
- {% include "_templates/flash.volt" %} + {% include "_components/flash.volt" %} {{ content() }}
{{ assets.outputJs() }} diff --git a/app/views/backend/_layouts/side-menu.volt b/app/views/backend/_layouts/side-menu.volt new file mode 100644 index 0000000..3208087 --- /dev/null +++ b/app/views/backend/_layouts/side-menu.volt @@ -0,0 +1,19 @@ + + + + + diff --git a/app/views/backend/log/index.volt b/app/views/backend/log/index.volt new file mode 100644 index 0000000..c0a8535 --- /dev/null +++ b/app/views/backend/log/index.volt @@ -0,0 +1,34 @@ + +

Activity Log

+ + + + + + + + + + + + + {% for item in page.items %} + + + + + + + {% endfor %} + +
DateIpUserMessage
{{ item.getTimestamp() }}{{ item.getIp() }} + {% if item.getUser() %} + {{ item.getUser().getId() }}:{{ item.getUser().getUsername() }} + {% else %} + - + {% endif %} + {{ item.getMessage() }}
+ + diff --git a/app/views/backend/user/index.volt b/app/views/backend/user/index.volt new file mode 100644 index 0000000..5ccaad4 --- /dev/null +++ b/app/views/backend/user/index.volt @@ -0,0 +1,33 @@ + +

Users

+ + + + + + + + + + + + + + + + {% for item in page.items %} + + + + + + + + + {% endfor %} + +
#UsernameNameEmailTypeStatus
{{ item.id }}{{ item.username }}{{ item.name }}{{ item.email }}{{ item.type }}{{ item.status }}
+ + diff --git a/app/views/auth/index.volt b/app/views/main/auth/index.volt similarity index 100% rename from app/views/auth/index.volt rename to app/views/main/auth/index.volt diff --git a/app/views/auth/register.volt b/app/views/main/auth/register.volt similarity index 100% rename from app/views/auth/register.volt rename to app/views/main/auth/register.volt diff --git a/app/views/callback/created.volt b/app/views/main/callback/created.volt similarity index 100% rename from app/views/callback/created.volt rename to app/views/main/callback/created.volt diff --git a/app/views/callback/list.volt b/app/views/main/callback/list.volt similarity index 100% rename from app/views/callback/list.volt rename to app/views/main/callback/list.volt diff --git a/app/views/callback/new.volt b/app/views/main/callback/new.volt similarity index 100% rename from app/views/callback/new.volt rename to app/views/main/callback/new.volt diff --git a/app/views/callback/show.volt b/app/views/main/callback/show.volt similarity index 100% rename from app/views/callback/show.volt rename to app/views/main/callback/show.volt diff --git a/app/views/error/error.volt b/app/views/main/error/error.volt similarity index 100% rename from app/views/error/error.volt rename to app/views/main/error/error.volt diff --git a/app/views/error/show404.volt b/app/views/main/error/show404.volt similarity index 100% rename from app/views/error/show404.volt rename to app/views/main/error/show404.volt diff --git a/app/views/index/about.volt b/app/views/main/index/about.volt similarity index 100% rename from app/views/index/about.volt rename to app/views/main/index/about.volt diff --git a/app/views/index/index.volt b/app/views/main/index/index.volt similarity index 100% rename from app/views/index/index.volt rename to app/views/main/index/index.volt diff --git a/app/views/user/activity.volt b/app/views/main/user/activity.volt similarity index 100% rename from app/views/user/activity.volt rename to app/views/main/user/activity.volt diff --git a/app/views/user/settings.volt b/app/views/main/user/settings.volt similarity index 100% rename from app/views/user/settings.volt rename to app/views/main/user/settings.volt diff --git a/docs/ACL.md b/docs/ACL.md new file mode 100644 index 0000000..1e3ccba --- /dev/null +++ b/docs/ACL.md @@ -0,0 +1,68 @@ + +# ACL + +The ACL is defined as follows: + +## Roles + +Roles define a group of user. like Author, Admin, Guest etc. +Each role can inherit other roles with the "inherit" key. +Each role can gain access to a zone (explained later) by the +"allowed-zones" key. Per default a role is denied access to all zones. + +## Resources + +Resources maps directly to `controller` names. If a controller is not +under the default module. `/` format is used instead. + +A special wildcard `*` character can be used to allow access to all +controllers (most likely only useful for non-default modules). + +For example the resource `backend/*` Matches all controllers under +the backend module. + +### Special controllers. + +There a 2 controllers that are a bit special, +`index` and `error` resources are always accessible by everyone (e.g. they +are not part of the ACL). + +## Access levels. + +These are not used in this system. a hardcoded "All" level is used. + +## Zones + +Zones defines as 1 or more resources. for example an "backend" zone can +have 2 controllers/resources (*site-config*, *user-manager*) + +Zones can also defines entire modules + + +# Example config. + +acl.yml +```yaml +acl: + roles: + guest: # Guests are only allowed to access the public zone. + allowed-zones: public + description: Non logged in users + user: # Users inherits the guest role + has access to user zone. + inherits: guest + allowed-zones: user + description: Logged in users + admin: # Admins inherits the user role + has access to backend zone. + inherits: user + description: Administrators + allowed-zones: backend + + zones: + # Public zone is the start page in + # index controller + login/logout in auth. + public: [ auth ] + # User zone can access profile and settings controllers + user: [ profile, settings ] + # Backend zone is the entire backend module. + backend: backend/* +```