Archived
1
0
Fork 0

Merge branch '24-admin-section' into 'dev'

Resolve "Admin section"

Closes #24

See merge request pnx/httpcb!25
This commit is contained in:
Henrik Hautakoski 2019-12-01 23:04:48 +00:00
commit 5867103646
42 changed files with 481 additions and 45 deletions

View file

@ -44,3 +44,4 @@
@import "views/about";
@import "views/login";
@import "views/register";
@import "views/backend";

View file

@ -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;
}
}

View file

@ -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/*

View file

@ -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/

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,21 @@
<?php
namespace App\Controller\Backend;
use App\Model\Data\ActivityLog;
class LogController extends \Phalcon\Mvc\Controller
{
public function onConstruct()
{
$this->view->setLayout('side-menu');
}
public function indexAction($page = 1)
{
$paginator = ActivityLog::getAllPaginationList($page);
$this->view->page = $paginator->getPaginate();
$this->view->pagination_url = '/admin/log/';
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Controller\Backend;
use App\Model\Data\User;
class UserController extends \Phalcon\Mvc\Controller
{
public function onConstruct()
{
$this->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();
}
}

View file

@ -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');
}
}

View file

@ -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();
}
}

View file

@ -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 {

View file

@ -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);
}

View file

@ -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 "<module>/<controller>"
return "{$dispatcher->getModuleName()}/{$dispatcher->getControllerName()}";
}
}

View file

@ -0,0 +1,19 @@
<?php
use Phinx\Migration\AbstractMigration;
class UserType extends AbstractMigration
{
public function up()
{
$this->table('user')
->addColumn('type', 'enum', [
'null' => false,
'default' => 'user',
'values' => [ 'user', 'admin' ],
'after' => 'regdate'
])
->save();
}
}

View file

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

View file

@ -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;
}
}

12
app/modules/Backend.php Normal file
View file

@ -0,0 +1,12 @@
<?php
namespace App\Module;
class Backend extends Base
{
protected $_controllerPath = APP_PATH . '/app/controllers/backend';
protected $_controllerNamespace = 'App\Controller\Backend';
protected $_viewDir = '../app/views/backend/';
}

47
app/modules/Base.php Normal file
View file

@ -0,0 +1,47 @@
<?php
namespace App\Module;
use Phalcon\Loader,
Phalcon\DiInterface,
Phalcon\Mvc\Dispatcher,
Phalcon\Mvc\ModuleDefinitionInterface;
abstract class Base implements ModuleDefinitionInterface
{
protected $_controllerPath;
protected $_controllerNamespace;
protected $_viewDir;
/**
* Register a specific autoloader for the module
*/
public function registerAutoloaders(DiInterface $di = null)
{
$loader = new Loader();
$loader->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()
));
}
}

12
app/modules/Main.php Normal file
View file

@ -0,0 +1,12 @@
<?php
namespace App\Module;
class Main extends Base
{
protected $_controllerPath = APP_PATH . '/app/controllers';
protected $_controllerNamespace = 'App\Controller';
protected $_viewDir = '../app/views/main/';
}

View file

@ -12,7 +12,7 @@
<header class="head-section">
<div class="top-section">
{% include "_templates/navigation.volt" %}
{% include "_components/navigation.volt" %}
</div>
{% block masthead %}{% endblock %}
@ -20,14 +20,14 @@
<main class="content-section">
{% include "_templates/flash.volt" %}
{% include "_components/flash.volt" %}
{{ content() }}
</main>
<div class="footer-section">
{% include "_templates/footer.volt" %}
{% include "_components/footer.volt" %}
</div>
{{ assets.outputJs() }}

View file

@ -0,0 +1,19 @@
<div class="backend">
<ul class="backend-sidemenu">
<li><a href="{{ url('/admin') }}">
{{ icon('solid/users') }} Users
</a></li>
<li><a href="{{ url('/admin/log') }}">
{{ icon('solid/bars') }} Log
</a></li>
</ul>
<div class="backend-content">
{{ content() }}
</div>
</div>

View file

@ -0,0 +1,34 @@
<h1>Activity Log</h1>
<table class="table table-condensed table-striped table-hover">
<thead>
<tr>
<th>Date</th>
<th>Ip</th>
<th>User</th>
<th>Message</th>
</tr>
</thead>
<tbody>
{% for item in page.items %}
<tr>
<td>{{ item.getTimestamp() }}</td>
<td>{{ item.getIp() }}</td>
<td>
{% if item.getUser() %}
{{ item.getUser().getId() }}:{{ item.getUser().getUsername() }}
{% else %}
-
{% endif %}
</td>
<td>{{ item.getMessage() }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<nav class="text-center" aria-label="Page navigation">
{{ partial('pagination') }}
</nav>

View file

@ -0,0 +1,33 @@
<h1>Users</h1>
<table class="table table-striped table-hover">
<thead>
<tr>
<th>#</th>
<th>Username</th>
<th>Name</th>
<th>Email</th>
<th>Type</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for item in page.items %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.username }}</td>
<td>{{ item.name }}</td>
<td>{{ item.email }}</td>
<td>{{ item.type }}</td>
<td>{{ item.status }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<nav class="text-center" aria-label="Page navigation">
{{ partial('pagination') }}
</nav>

68
docs/ACL.md Normal file
View file

@ -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. `<module>/<controller>` 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/*
```