Premier commit

This commit is contained in:
2024-09-09 10:22:45 +02:00
commit bcc2604080
74 changed files with 25819 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
<?php
/**
* This file is part of the Piko user module
*
* @copyright 2020 Sylvain PHILIP.
* @license LGPL-3.0; see LICENSE.txt
* @link https://github.com/piko-framework/piko-user
*/
namespace app\modules\user;
use app\modules\user\models\User;
/**
* Access checker class
*
* @author Sylvain PHILIP <contact@sphilip.com>
*/
class AccessChecker
{
public static $adminRole;
/**
* User roles
*
* @var null|array
*/
private static $roles = null;
/**
* User permissions
*
* @var null|array
*/
private static $permissions = null;
/**
* Check Permission or role access
*
* @param int $userId The user Id
* @param string $permission The permission or role name
* @return bool
*
* @see \piko\User
*/
public static function checkAccess($userId, string $permission) : bool
{
$identity = User::findIdentity($userId);
if ($identity !== null) {
if (static::$roles === null) {
static::$roles = Rbac::getUserRoles($identity->id);
}
if (in_array(static::$adminRole, static::$roles)) {
return true;
}
if (in_array($permission, static::$roles)) {
return true;
}
if (static::$permissions === null) {
static::$permissions = Rbac::getUserPermissions($identity->id);
}
if (in_array($permission, static::$permissions)) {
return true;
}
}
return false;
}
}

73
modules/user/Module.php Normal file
View File

@@ -0,0 +1,73 @@
<?php
/**
* This file is part of the Piko user module
*
* @copyright 2020 Sylvain PHILIP.
* @license LGPL-3.0; see LICENSE.txt
* @link https://github.com/piko-framework/piko-user
*
* Routes :
* /user/default/login : Process login
* /user/default/logout : Process logout
* /user/default/register : Process user registration
* /user/default/edit : User account form
* /user/admin/users : Manage users, roles, permissions
*/
namespace app\modules\user;
use PDO;
use app\modules\user\models\User;
use app\modules\user\Rbac;
/**
* User module class
*
* @author Sylvain PHILIP <contact@sphilip.com>
*/
class Module extends \Piko\Module
{
/**
* The admin role
* @var string
*/
public $adminRole = 'admin';
/**
* Allow user registration
*
* @var boolean
*/
public $allowUserRegistration = false;
/**
* Min length of the user password
*
* @var integer
*/
public $passwordMinLength = 8;
public function bootstrap()
{
$pdo = $this->application->getComponent('PDO');
assert($pdo instanceof PDO);
User::$pdo = $pdo;
Rbac::$db = $pdo;
AccessChecker::$adminRole = $this->adminRole;
}
/**
* {@inheritDoc}
* @see \piko\Module::init()
*/
protected function init()
{
/* @var $i18n \piko\i18n */
// $i18n = Piko::get('i18n');
// $i18n->addTranslation('user', __DIR__ . '/messages');
// parent::init();
}
}

263
modules/user/Rbac.php Normal file
View File

@@ -0,0 +1,263 @@
<?php
/**
* This file is part of the Piko user module
*
* @copyright 2020 Sylvain PHILIP.
* @license LGPL-3.0; see LICENSE.txt
* @link https://github.com/piko-framework/piko-user
*/
namespace app\modules\user;
use PDO;
/**
* Rbac utility class
*
* @author Sylvain PHILIP <contact@sphilip.com>
*/
class Rbac
{
public static PDO $db;
/**
* Create a role
*
* @param string $name The role name
* @param string $description The role description
* @return int The role Id
*/
public static function createRole($name, $description = '')
{
$query = 'INSERT INTO `auth_role` (`name`, `description`) VALUES (?, ?)';
static::$db->beginTransaction();
$st = static::$db->prepare($query);
$st->execute([$name, $description]);
static::$db->commit();
return static::$db->lastInsertId();
}
/**
* Check if the role exists
*
* @param string $name The role name
* @return boolean
*/
public static function roleExists($name)
{
$st = static::$db->prepare('SELECT COUNT(`id`) FROM `auth_role` WHERE `name` = :name');
$st->execute(['name' => $name]);
return ((int) $st->fetchColumn() > 0) ? true : false;
}
/**
* Get the role Id
*
* @param string $name The role name
* @return int The role Id (0 if the role is not found)
*/
public static function getRoleId($name)
{
$st = static::$db->prepare('SELECT `id` FROM `auth_role` WHERE `name` = :name');
$st->execute(['name' => $name]);
return (int) $st->fetchColumn();
}
/**
* Assign a role to an user
*
* @param int $userId The user Id
* @param string $roleName The role name
* @throws \RuntimeException If the role doesn't exists
*/
public static function assignRole($userId, $roleName)
{
$roleId = static::getRoleId($roleName);
if (!$roleId) {
throw new \RuntimeException("Role $roleName doesn't exists");
}
$query = 'INSERT INTO `auth_assignment` (`role_id`, `user_id`) VALUES (?, ?)';
static::$db->beginTransaction();
$st = static::$db->prepare($query);
$st->execute([$roleId, $userId]);
static::$db->commit();
}
/**
* Get user roles
*
* @param int $userId The user Id
* @return array An array containing user roles
*/
public static function getUserRoles($userId)
{
$query = 'SELECT `auth_role`.`name` FROM `auth_role` '
. 'INNER JOIN `auth_assignment` ON `auth_assignment`.`role_id` = `auth_role`.`id` '
. 'WHERE `auth_assignment`.`user_id` = :user_id '
. 'GROUP BY role_id';
$st = static::$db->prepare($query);
$st->execute(['user_id' => $userId]);
return $st->fetchAll(\PDO::FETCH_COLUMN);
}
/**
* Get user roles ids
*
* @param int $userId The user Id
* @return array An array containing user role ids
*/
public static function getUserRoleIds($userId)
{
$query = 'SELECT role_id FROM `auth_assignment` WHERE user_id = :user_id';
$sth = static::$db->prepare($query);
$sth->execute(['user_id' => $userId]);
return $sth->fetchAll(\PDO::FETCH_COLUMN);
}
/**
* Get user permissions
*
* @param int $userId The user Id
* @return array An array containing user permissions
*/
public static function getUserPermissions($userId)
{
$query = 'SELECT p.`name` FROM `auth_permission` AS p '
. 'INNER JOIN `auth_role_has_permission` AS ap ON ap.`permission_id` = p.`id` '
. 'INNER JOIN `auth_assignment` AS aa ON aa.`role_id` = ap.`role_id` '
. 'WHERE aa.`user_id` = :user_id '
. 'GROUP BY permission_id';
$st = static::$db->prepare($query);
$st->execute(['user_id' => $userId]);
return $st->fetchAll(\PDO::FETCH_COLUMN);
}
/**
* Get role permissions
*
* @param string $roleName The role name
* @return array An array of permissions as string
*/
public static function getRolePermissions($roleName): array
{
$roleId = static::getRoleId($roleName);
if (!$roleId) {
throw new \RuntimeException("Role $roleName doesn't exists");
}
$query = 'SELECT p.`name` FROM `auth_permission` AS p '
. 'INNER JOIN `auth_role_has_permission` AS ap ON ap.`permission_id` = p.`id` '
. 'WHERE ap.`role_id` = :role_id '
. 'GROUP BY permission_id';
$st = static::$db->prepare($query);
$st->execute(['role_id' => $roleId]);
return $st->fetchAll(\PDO::FETCH_COLUMN);
}
/**
* Get role permission ids
*
* @param string $roleName The role name
* @return array An array of permission ids
*/
public static function getRolePermissionIds($roleName)
{
$roleId = static::getRoleId($roleName);
if (!$roleId) {
throw new \RuntimeException("Role $roleName doesn't exists");
}
$query = 'SELECT permission_id FROM `auth_role_has_permission` WHERE role_id = :role_id';
$sth = static::$db->prepare($query);
$sth->execute(['role_id' => $roleId]);
return $sth->fetchAll(\PDO::FETCH_COLUMN);
}
/**
* Create a permission
*
* @param string $name The permission name
* @return int The permission id
*/
public static function createPermission($name): int
{
$query = 'INSERT INTO `auth_permission` (`name`) VALUES (?)';
static::$db->beginTransaction();
$st = static::$db->prepare($query);
$st->execute([$name]);
static::$db->commit();
return (int) static::$db->lastInsertId();
}
/**
* Check if the permission exists
*
* @param string $name The permission name
* @return boolean
*/
public static function permissionExists($name): bool
{
$st = static::$db->prepare('SELECT COUNT(`id`) FROM `auth_permission` WHERE `name` = :name');
$st->execute(['name' => $name]);
return ((int) $st->fetchColumn() > 0) ? true : false;
}
/**
* Get the permission Id
*
* @param string $name The permission name
* @return int The permission id (0 if the permission is not found)
*/
public static function getPermissionId($name): int
{
$st = static::$db->prepare('SELECT `id` FROM `auth_permission` WHERE `name` = :name');
$st->execute(['name' => $name]);
return (int) $st->fetchColumn();
}
/**
* Assign a permission to a role
*
* @param string $roleName The role name
* @param string $permissionName The permission name
* @throws \RuntimeException If the role or the permission doesn't exists
*/
public static function assignPermission($roleName, $permissionName): void
{
$roleId = static::getRoleId($roleName);
$permissionId = static::getPermissionId($permissionName);
if (!$roleId) {
throw new \RuntimeException("Role $roleName doesn't exists");
}
if (!$permissionId) {
throw new \RuntimeException("Permission $permissionName doesn't exists");
}
$query = 'INSERT INTO `auth_role_has_permission` (`role_id`, `permission_id`) VALUES (?, ?)';
static::$db->beginTransaction();
$st = static::$db->prepare($query);
$st->execute([$roleId, $permissionId]);
static::$db->commit();
}
}

View File

@@ -0,0 +1,248 @@
<?php
/**
* This file is part of the Piko user module
*
* @copyright 2020 Sylvain PHILIP.
* @license LGPL-3.0; see LICENSE.txt
* @link https://github.com/piko-framework/piko-user
*/
namespace app\modules\user\controllers;
use Piko\HttpException;
use function Piko\I18n\__;
use Piko\User as PikoUser;
use app\modules\user\models\Role;
use app\modules\user\models\User;
use app\modules\user\models\Permission;
/**
* User admin controller
*
* @author Sylvain PHILIP <contact@sphilip.com>
*/
class AdminController extends \Piko\Controller
{
protected PikoUser $user;
protected \PDO $db;
public function init(): void
{
$app = $this->module->getApplication();
$user = $app->getComponent('Piko\User');
assert($user instanceof PikoUser);
$this->user = $user;
$db = $app->getComponent('PDO');
assert($db instanceof \PDO);
$this->db = $db;
}
/**
* {@inheritDoc}
* @see \piko\Controller::runAction()
*/
public function runAction($id)
{
assert($this->module instanceof \app\modules\user\Module);
if (!$this->user->can($this->module->adminRole)) {
throw new HttpException('Not authorized.', 403);
}
return parent::runAction($id);
}
/**
* Render users view
*
* @return string
*/
public function usersAction()
{
return $this->render('users', [
'users' => User::find()
]);
}
/**
* Render User form and create or update user
*
* @return string
*/
public function editAction(int $id = 0)
{
$user = new User($this->db);
if ($id) {
$user->load($id);
}
$user->scenario = User::SCENARIO_ADMIN;
$message = false;
$post = $this->request->getParsedBody();
if (!empty($post)) {
$user->bind($post);
if ($user->isValid() && $user->save()) {
$message['type'] = 'success';
$message['content'] = __('user', 'User successfully saved');
} else {
$message['type'] = 'danger';
$message['content'] = __('user', 'Save error!') . implode(' ', $user->errors);
}
}
return $this->render('edit', [
'user' => $user,
'message' => $message,
'roles' => Role::find('`name` ASC'),
]);
}
/**
* Delete users
*/
public function deleteAction()
{
$post = $this->request->getParsedBody();
$ids = isset($post['items'])? $post['items'] : [];
foreach ($ids as $id) {
$user = new User($id);
$user->delete();
}
$this->redirect($this->getUrl('user/admin/users'));
}
/**
* Render roles view
*
* @return string
*/
public function rolesAction()
{
return $this->render('roles', [
'roles' => Role::find(),
'permissions' => Permission::find('`name` ASC'),
]);
}
/**
* Create/update role (AJAX)
*
* @return string
*/
public function editRoleAction(int $id = 0)
{
$role = new Role($this->db);
if ($id) {
$role->load($id);
}
$role->scenario = Role::SCENARIO_ADMIN;
$post = $this->request->getParsedBody();
$response = [
'role' => $role
];
if (!empty($post)) {
$role->bind($post);
if ($role->isValid() && $role->save()) {
$response['status'] = 'success';
} else {
$response['status'] = 'error';
}
}
return $this->jsonResponse($response);
}
/**
* Delete roles
*/
public function deleteRolesAction()
{
$post = $this->request->getParsedBody();
$ids = isset($post['items'])? $post['items'] : [];
foreach ($ids as $id) {
$item = new Role($id);
$item->delete();
}
$this->redirect($this->getUrl('user/admin/roles'));
}
/**
* Render permissions view
*
* @return string
*/
public function permissionsAction()
{
return $this->render('permissions', [
'permissions' => Permission::find()
]);
}
/**
* Create/update permission (AJAX)
*
* @return string
*/
public function editPermissionAction(int $id = 0)
{
$permission = new Permission($this->db);
if ($id) {
$permission->load($id);
}
$response = [
'permission' => $permission
];
$post = $this->request->getParsedBody();
if (!empty($post)) {
$permission->bind($post);
if ($permission->isValid() && $permission->save()) {
$response['status'] = 'success';
} else {
$response['status'] = 'error';
$response['error'] = array_pop($permission->getErrors());
}
}
return $this->jsonResponse($response);
}
/**
* Delete permissions
*/
public function deletePermissionsAction()
{
$post = $this->request->getParsedBody();
$ids = isset($post['items'])? $post['items'] : [];
foreach ($ids as $id) {
$item = new Permission($id);
$item->delete();
}
$this->redirect($this->getUrl('user/admin/permissions'));
}
}

View File

@@ -0,0 +1,295 @@
<?php
/**
* This file is part of the Piko user module
*
* @copyright 2020 Sylvain PHILIP.
* @license LGPL-3.0; see LICENSE.txt
* @link https://github.com/piko-framework/piko-user
*/
namespace app\modules\user\controllers;
use function Piko\I18n\__;
use piko\HttpException;
use app\modules\user\models\User;
use Piko\User as PikoUser;
/**
* User default controller
*
* @author Sylvain PHILIP <contact@sphilip.com>
*/
class DefaultController extends \Piko\Controller
{
protected PikoUser $user;
protected \PDO $db;
public function init(): void
{
$app = $this->module->getApplication();
$user = $app->getComponent('Piko\User');
assert($user instanceof PikoUser);
$this->user = $user;
$db = $app->getComponent('PDO');
assert($db instanceof \PDO);
$this->db = $db;
}
/**
* Render and process user registration
*
* @return string
*/
public function registerAction()
{
if (!$this->user->isGuest()) {
return $this->redirect('/');
}
$message = false;
$post = $this->request->getParsedBody();
if (!empty($post)) {
$user = new User($this->db);
$user->scenario = User::SCENARIO_REGISTER;
$user->bind($post);
if ($user->isValid() && $user->save()) {
// $user->sendRegistrationConfirmation();
$message['type'] = 'success';
$message['content'] = __(
'user',
'Your account was created. Please activate it through the confirmation email that was sent to you.'
);
} else {
$message['type'] = 'danger';
$message['content'] = implode(', ', $user->errors);
}
}
return $this->render('register', [
'message' => $message,
]);
}
/**
* Validate registration (AJAX)
*
* @return string
*/
public function checkRegistrationAction()
{
$errors = [];
$this->layout = false;
$post = $this->request->getParsedBody();
if (!empty($post)) {
$user = new User($this->db);
$user->scenario = 'register';
$user->bind($post);
$user->isValid();
$errors = $user->getErrors();
}
return $this->jsonResponse($errors);
}
/**
* Render user activation confirmation
*
* @throws HttpException
* @return string
*/
public function confirmationAction($token)
{
$user = User::findByAuthKey($token);
if (!$user) {
throw new HttpException('Not found.', 404);
}
$message = false;
if (!$user->isActivated()) {
if ($user->activate()) {
$message['type'] = 'success';
$message['content'] = __('user', 'Your account has been activated. You can now log in.');
} else {
$message['type'] = 'danger';
$message['content'] = __(
'user',
'Unable to activate your account. Please contact the site manager.'
);
}
} else {
$message['type'] = 'warning';
$message['content'] = __('user', 'Your account has already been activated.');
}
return $this->render('login', ['message' => $message]);
}
/**
* Render reminder password form and send email to change password
*
* @return string
*/
public function reminderAction()
{
$message = false;
$post = $this->request->getParsedBody();
$reminder = $post['reminder']?? '';
if (!empty($reminder)) {
$user = User::findByUsername($reminder);
if (!$user) {
$user = User::findByEmail($reminder);
}
if ($user) {
// $user->sendResetPassword();
$message['type'] = 'success';
$message['content'] = __(
'user',
'A link has been sent to you by email ({email}). It will allow you to recreate your password.',
['email' => $user->email]
);
} else {
$message['type'] = 'danger';
$message['content'] = __('user', 'Account not found.');
}
}
return $this->render('reminder', [
'message' => $message,
'reminder' => $reminder,
]);
}
/**
* Render and process reset password
*
* @throws HttpException
* @return string
*/
public function resetPasswordAction($token)
{
$user = User::findByAuthKey($token);
if (!$user) {
throw new HttpException('Not found', 404);
}
$message = false;
$post = $this->request->getParsedBody();
if (!empty($post)) {
$user->scenario = 'reset';
$user->bind($post);
if ($user->isValid() && $user->save()) {
$message['type'] = 'success';
$message['content'] = __('user', 'Your password has been successfully updated.');
} else {
$message['type'] = 'danger';
$message['content'] = implode(', ', $user->errors);
}
}
return $this->render('reset', [
'message' => $message,
'user' => $user,
]);
}
/**
* Render user form and update changes
*
* @throws HttpException
* @return string
*/
public function editAction()
{
if ($this->user->isGuest()) {
throw new HttpException(__('user', 'You must be logged to access this page.'), 401);
}
$identity = $this->user->getIdentity();
assert($identity instanceof User);
$message = false;
$post = $this->request->getParsedBody();
if (!empty($post)) {
$identity->bind($post);
if ($identity->isValid() && $identity->save()) {
$message['type'] = 'success';
$message['content'] = __('user', 'Changes saved!');
} else {
$message['type'] = 'danger';
$message['content'] = implode(', ', $identity->getErrors());
}
}
return $this->render('edit', [
'user' => $identity,
'message' => $message,
]);
}
/**
* Render login form and process login
*
* @return string
*/
public function loginAction()
{
$message = false;
$post = $this->request->getParsedBody();
if (!empty($post)) {
$identity = User::findByUsername($post['username']);
if ($identity instanceof User && $identity->validatePassword($post['password'])) {
$this->user->login($identity);
$identity->last_login_at = time();
$identity->save();
return $this->redirect('/');
} else {
$message['type'] = 'danger';
$message['content'] = __('user', 'Authentication failure');
}
}
assert($this->module instanceof \app\modules\user\Module);
return $this->render('login', [
'message' => $message,
'canRegister' => $this->module->allowUserRegistration
]);
}
/**
* User logout
*/
public function logoutAction()
{
$this->user->logout();
$this->redirect('/');
}
}

View File

@@ -0,0 +1,111 @@
<?php
$confirmationMailBody = <<<MSG
Bonjour,
Merci de vous être inscrit sur {site_name}. Votre compte a été créé et doit être activé avant que vous puissiez l'utiliser.
Pour l'activer, cliquez sur le lien ci-dessous ou copiez et collez le dans votre navigateur :
{link}
Après activation vous pourrez vous connecter sur {base_url} en utilisant l'identifiant suivant et le mot de passe utilisé à l'enregistrement :
Identifiant : {username}
MSG;
$resetPasswordMailBody = <<<MSG
Bonjour,
Une demande de changement de mot passe a été effectuée pour votre compte sur {site_name}.
Votre identifiant est : {username}.
Pour changer votre mot de passe , cliquez sur le lien ci-dessous.
{link}
Merci.
MSG;
return [
'Users' => 'Utilisateurs',
'Name' => 'Nom',
'Username' => 'Identifiant',
'Email' => 'Email',
'Password' => 'Mot de passe',
'Last login at' => 'Dernière connexion',
'Created at' => 'Créé le',
'Id' => 'Id',
'Roles' => 'Rôles',
'Role name' => 'Nom du rôle',
'Role name must be filled in.' => 'Le nom du role doit être renseigné.',
'Role already exists.' => 'Le rôle existe déjà.',
'Description' => 'Description',
'Role permissions' => 'Permissions du rôle',
'Users management' => 'Gestion des utilisateurs',
'Are you sure you want to perform this action?' => 'Êtes-vous certain de vouloir effectuer cette action ?',
'Create user' => 'Nouvel utilisateur',
'Edit user' => 'Modifier l\'utilisateur',
'New role' => 'Nouveau rôle',
'Delete' => 'Supprimer',
'Close' => 'Fermer',
'Cancel' => 'Annuler',
'Permissions' => 'Permissions',
'New permission' => 'Nouvelle permission',
'Permission name' => 'Nom de la permission',
'Permission name must be filled in.' => 'Le nom de la permission doit être renseigné.',
'Permission already exists.' => 'La permission existe déjà',
'User successfully saved' => 'Utilisateur correctement enregistré',
'Save' => 'Enregistrer',
'Save error!' => 'Erreur lors de l\'enregistrement',
'Email must be filled in.' => 'L\'email doit être renseigné.',
'{email} is not a valid email address.' => '{email} n\'est pas une adresse email valide.',
'Username must be filled in.' => 'Le nom d\'utilisateur doit être renseigné.',
'The username should only contain alphanumeric characters.' => 'Le nom d\'utilisateur ne doit contenir que des caractères alphanumériques.',
'This email is already used.' => 'Cet email est déjà utilisé.',
'This username is already used.' => 'Cet identifant est déjà utilisé.',
'Password must be filled in.' => 'Le mot de passe doit être renseigné.',
'Password is to short. Minimum {num}: characters.' => 'Mot de passe trop court. Minimum {num} caractères.',
'Passwords are not the same.' => 'Les mots de passe ne sont pas identiques.',
// Register Account
'Your account was created. Please activate it through the confirmation email that was sent to you.' => 'Votre compte a été créé. Merci de l\'activer via le mail de confirmation qui vous a été envoyé.',
'confirmation_mail_body' => $confirmationMailBody,
'Registration confirmation on {site_name}' => 'Confirmation de l\'inscription sur {site_name}',
// Account activation
'Your account has been activated. You can now log in.' => 'Votre compte a bien été activé. Vous pouvez désormais vous connecter.',
'Unable to activate your account. Please contact the site manager.' => 'Impossible d\'activer votre compte. Merci de contacter le responsable du site.',
'Your account has already been activated.' => 'Votre compte a déjà été activé.',
// Password reset / reminder
'A link has been sent to you by email ({email}). It will allow you to recreate your password.' => 'Un lien vous a été envoyé par email ({email}). Il vous permettra de recréer votre mot de passe.',
'reset_password_mail_body' => $resetPasswordMailBody,
'Password change request on {site_name}' => 'Demande de changement de mot de passe sur {site_name}',
'Account not found.' => 'Compte innexistant',
'Your password has been successfully updated.' => 'Votre mot de passe a bien été modifié.',
'Forget password' => 'Mot de passe oublié',
'Your email or your username' => 'Votre email ou votre identifiant',
'Send' => 'Envoyer',
'Change your account ({account}) password' => 'Réinitialisation du mot de passe pour le compte : {account}',
// Edit account
'You must be logged to access this page.' => 'Vous devez vous connecter pour accéder à cette page.',
'Changes saved!' => 'Modifications enregistrées !',
'Edit your account' => 'Modification de votre compte',
'Password (leave blank to keep the same)' => 'Mot de passe (laisser vide pour garder le même)',
'Last name' => 'Nom',
'First name' => 'Prénom',
'Company' => 'Entreprise',
'Phone number' => 'Téléphone',
'Address' => 'Adresse',
'Zip code' => 'Code postal',
'City' => 'Ville',
'Country' => 'Pays',
// Login
'Authentication failure' => 'Échec de l\'authentification.',
'Login' => 'Connexion',
'No account yet?' => 'Pas encore de compte ?',
'Register' => 'Créer un compte',
'Forget password?' => 'Mot de passe oublié ?',
// register
'Confirm your password' => 'Confirmez votre mot de passe'
];

View File

@@ -0,0 +1,96 @@
<?php
/**
* This file is part of the Piko user module
*
* @copyright 2020 Sylvain PHILIP.
* @license LGPL-3.0; see LICENSE.txt
* @link https://github.com/piko-framework/piko-user
*/
namespace app\modules\user\models;
use function Piko\I18n\__;
/**
* This is the model class for table "auth_permission".
*
* @property integer $id
* @property string $name;
*
* @author Sylvain PHILIP <contact@sphilip.com>
*/
class Permission extends \piko\DbRecord
{
/**
* The table name
*
* @var string
*/
protected $tableName = 'auth_permission';
/**
* The model errors
*
* @var array
*/
public $errors = [];
/**
* The table schema
*
* @var array
*/
protected $schema = [
'id' => self::TYPE_INT,
'name' => self::TYPE_STRING,
];
/**
* {@inheritDoc}
* @see \Piko\ModelTrait::validate()
*/
protected function validate(): void
{
if (empty($this->name)) {
$this->errors['name'] = __('user', 'Permission name must be filled in.');
} else {
$st = $this->db->prepare('SELECT COUNT(`id`) FROM `auth_permission` WHERE name = :name');
$st->execute(['name' => $this->name]);
$count = (int) $st->fetchColumn();
if ($count) {
$this->errors['name'] = __('user', 'Permission already exists.');
}
}
}
/**
* Find permissions
*
* @param string $order The order condition
* @param number $start The offset start
* @param number $limit The offset limit
*
* @return array An array of permission rows
*/
public static function find($order = '', $start = 0, $limit = 0)
{
$db = User::$pdo;
$query = 'SELECT `id`, `name` FROM `auth_permission`';
$query .= ' ORDER BY ' . (empty($order) ? '`id` DESC' : $order);
if (!empty($start)) {
$query .= ' OFFSET ' . (int) $start;
}
if (!empty($limit)) {
$query .= ' LIMIT ' . (int) $limit;
}
$sth = $db->prepare($query);
$sth->execute();
return $sth->fetchAll();
}
}

View File

@@ -0,0 +1,180 @@
<?php
/**
* This file is part of the Piko user module
*
* @copyright 2020 Sylvain PHILIP.
* @license LGPL-3.0; see LICENSE.txt
* @link https://github.com/piko-framework/piko-user
*/
namespace app\modules\user\models;
use function Piko\I18n\__;
use app\modules\user\Rbac;
/**
* This is the model class for table "auth_role.
*
* @property integer $id
* @property integer $parent_id
* @property string $name;
* @property string $description;
*
* @author Sylvain PHILIP <contact@sphilip.com>
*/
class Role extends \Piko\DbRecord
{
const SCENARIO_ADMIN = 'admin';
/**
* The table name
*
* @var string
*/
protected $tableName = 'auth_role';
/**
* The model scenario
*
* @var string
*/
public $scenario = '';
/**
* The model errors
*
* @var array
*/
public $errors = [];
/**
* The role permissions
*
* @var array
*/
public $permissions = [];
/**
* The table schema
*
* @var array
*/
protected $schema = [
'id' => self::TYPE_INT,
'name' => self::TYPE_STRING,
'description' => self::TYPE_STRING,
];
/**
* {@inheritDoc}
* @see \piko\Component::init()
*/
protected function init()
{
if (!empty($this->name)) {
$this->permissions = Rbac::getRolePermissionIds($this->name);
}
}
/**
* {@inheritDoc}
* @see \Piko\DbRecord::bind()
*/
public function bind($data): void
{
if (isset($data['permissions'])) {
$this->permissions = $data['permissions'];
unset($data['permissions']);
}
parent::bind($data);
}
/**
* {@inheritDoc}
* @see \Piko\DbRecord::afterSave()
*/
protected function afterSave(): void
{
if ($this->scenario === self::SCENARIO_ADMIN) {
$st = $this->db->prepare('DELETE FROM `auth_role_has_permission` WHERE role_id = :role_id');
if (!$st->execute(['role_id' => $this->id])) {
throw new \RuntimeException(
"Error while trying to delete role id {$this->id} in auth_role_has_permission table"
);
}
if (!empty($this->permissions)) {
$values = [];
foreach ($this->permissions as $id) {
$values[] = '(' . (int) $this->id . ',' . (int) $id . ')';
}
$query = 'INSERT INTO `auth_role_has_permission` (role_id, permission_id) VALUES '
. implode(', ', $values);
$this->db->beginTransaction();
$st = $this->db->prepare($query);
$st->execute();
$this->db->commit();
}
}
parent::afterSave();
}
/**
* {@inheritDoc}
* @see \Piko\ModelTrait::validate()
*/
protected function validate(): void
{
if (empty($this->name)) {
$this->errors['name'] = __('user', 'Role name must be filled in.');
} else {
$st = $this->db->prepare('SELECT COUNT(`id`) FROM `auth_role` WHERE name = :name');
$st->execute(['name' => $this->name]);
$count = (int) $st->fetchColumn();
if ($count) {
$this->errors['name'] = __('user', 'Role already exists.');
}
}
}
/**
* Get roles
*
* @param string $order The order condition
* @param number $start The offset start
* @param number $limit The offset limit
*
* @return array An array of role rows
*/
public static function find($order = '', $start = 0, $limit = 0)
{
$db = User::$pdo;
$query = 'SELECT * FROM `auth_role`';
$query .= ' ORDER BY ' . (empty($order) ? '`id` DESC' : $order);
if (!empty($start)) {
$query .= ' OFFSET ' . (int) $start;
}
if (!empty($limit)) {
$query .= ' LIMIT ' . (int) $limit;
}
$sth = $db->prepare($query);
$sth->execute();
return $sth->fetchAll();
}
}

View File

@@ -0,0 +1,551 @@
<?php
/**
* This file is part of the Piko user module
*
* @copyright 2020 Sylvain PHILIP.
* @license LGPL-3.0; see LICENSE.txt
* @link https://github.com/piko-framework/piko-user
*/
namespace app\modules\user\models;
use function Piko\I18n\__;
use Piko\Router;
use app\modules\user\Rbac;
use app\modules\user\Module;
use Nette\Mail\Message;
use Nette\Mail\SmtpMailer;
use Nette\Utils\Random;
/**
* This is the model class for table "user".
*
* @property integer $id
* @property string $name;
* @property string $username;
* @property string $email;
* @property string $password;
* @property string $auth_key;
* @property integer $confirmed_at;
* @property integer $blocked_at;
* @property string $registration_ip;
* @property integer $created_at;
* @property integer $updated_at;
* @property integer $last_login_at;
* @property string $timezone;
* @property string $profil;
*
* @author Sylvain PHILIP <contact@sphilip.com>
*/
class User extends \Piko\DbRecord implements \Piko\User\IdentityInterface
{
const SCENARIO_ADMIN = 'admin';
const SCENARIO_REGISTER = 'register';
const SCENARIO_RESET = 'reset';
public static \PDO $pdo;
public static Module $module;
/**
* The table name
* @var string
*/
protected $tableName = 'user';
/**
* The model errors
*
* @var array
*/
public $errors = [];
/**
* The model scenario
*
* @var string
*/
public $scenario = '';
/**
* The user role ids
*
* @var array
*/
protected $roleIds = [];
/**
* The confirmation password
*
* @var string
*/
protected $password2 = '';
/**
* Reset password state
*
* @var boolean
*/
protected $resetPassword = false;
/**
* The table schema
*
* @var array
*/
protected $schema = [
'id' => self::TYPE_INT,
'name' => self::TYPE_STRING,
'username' => self::TYPE_STRING,
'email' => self::TYPE_STRING,
'password' => self::TYPE_STRING,
'auth_key' => self::TYPE_STRING,
'confirmed_at' => self::TYPE_INT,
'blocked_at' => self::TYPE_INT,
'registration_ip' => self::TYPE_STRING,
'created_at' => self::TYPE_INT,
'updated_at' => self::TYPE_INT,
'last_login_at' => self::TYPE_INT,
'is_admin' => self::TYPE_INT,
'timezone' => self::TYPE_STRING,
'profil' => self::TYPE_STRING,
];
/**
* {@inheritDoc}
* @see \Piko\DbRecord::beforeSave()
*/
protected function beforeSave($isNew): bool
{
if ($isNew) {
$this->name = $this->username;
$this->password = sha1($this->password);
$this->created_at = time();
$this->auth_key = sha1(Random::generate(10));
} else {
$this->updated_at = time();
if ($this->resetPassword) {
$this->password = sha1($this->password);
}
}
return parent::beforeSave($isNew);
}
/**
* {@inheritDoc}
* @see \Piko\DbRecord::afterSave()
*/
protected function afterSave(): void
{
if ($this->scenario === self::SCENARIO_ADMIN) {
// Don't allow admin user to remove its admin role
/*
if ($this->id == Piko::get('user')->getId()) {
$adminRole = Piko::get('userModule')->adminRole;
$adminRoleId = Rbac::getRoleId($adminRole);
if (!in_array($adminRoleId, $this->roleIds)) {
$this->roleIds[] = $adminRoleId;
}
}
*/
if (!empty($this->roleIds)) {
$roleIds = Rbac::getUserRoleIds($this->id);
$idsToRemove = array_diff($roleIds, $this->roleIds);
$idsToAdd = array_diff($this->roleIds, $roleIds);
if (!empty($idsToRemove)) {
$query = 'DELETE FROM `auth_assignment` WHERE user_id = :user_id AND role_id IN('
. implode(',', $idsToRemove) . ')';
$st = $this->db->prepare($query);
$st->execute(['user_id' => $this->id]);
}
if (!empty($idsToAdd)) {
$values = [];
foreach ($idsToAdd as $id) {
$values[] = '(' . (int) $this->id . ',' . (int) $id . ')';
}
$query = 'INSERT INTO `auth_assignment` (user_id, role_id) VALUES ' . implode(', ', $values);
$this->db->beginTransaction();
$st = $this->db->prepare($query);
$st->execute();
$this->db->commit();
}
} else {
$st = $this->db->prepare('DELETE FROM `auth_assignment` WHERE user_id = :user_id');
$st->execute(['user_id' => $this->id]);
}
}
parent::afterSave();
}
/**
* {@inheritDoc}
* @see \Piko\DbRecord::bind()
*/
public function bind($data): void
{
if (isset($data['password']) && empty($data['password'])) {
unset($data['password']);
}
if (isset($data['password2'])) {
$this->password2 = $data['password2'];
unset($data['password2']);
}
if (!empty($data['password']) && !$this->validatePassword($data['password'])) {
$this->resetPassword = true;
}
if (!empty($data['profil']) && is_array($data['profil'])) {
$data['profil'] = json_encode($data['profil']);
}
if (isset($data['roles']) && $this->scenario == self::SCENARIO_ADMIN) {
$this->roleIds = $data['roles'];
unset($data['roles']);
}
parent::bind($data);
}
/**
* {@inheritDoc}
* @see \Piko\ModeTrait::validate()
*/
protected function validate(): void
{
if (empty($this->email)) {
$this->errors['email'] = __('user', 'Email must be filled in.');
} elseif (!filter_var($this->email, FILTER_VALIDATE_EMAIL)) {
$this->errors['email'] = __(
'user',
'{email} is not a valid email address.',
['email' => $this->data['email']]
);
}
if (($this->scenario == self::SCENARIO_REGISTER || $this->scenario == self::SCENARIO_ADMIN)
&& empty($this->username)) {
$this->errors['username'] = __('user', 'Username must be filled in.') ;
}
// New user
if (($this->scenario == self::SCENARIO_REGISTER || $this->scenario == self::SCENARIO_ADMIN)
&& empty($this->id)) {
$st = $this->db->prepare('SELECT id FROM user WHERE email = ?');
$st->execute([$this->email]);
$id = $st->fetchColumn();
if ($id) {
$this->errors['email'] = __('user', 'This email is already used.');
}
$st = $this->db->prepare('SELECT id FROM user WHERE username = ?');
$st->execute([$this->username]);
$id = $st->fetchColumn();
if ($id) {
$this->errors['username'] = __('user', 'This username is already used.');
}
}
if (($this->scenario == self::SCENARIO_REGISTER || $this->scenario == self::SCENARIO_RESET)
&& empty($this->password)) {
$this->errors['password'] = __('user', 'Password must be filled in.');
} elseif (($this->scenario == self::SCENARIO_REGISTER || $this->scenario == self::SCENARIO_RESET) &&
strlen($this->password) < static::$module->passwordMinLength) {
$this->errors['password'] = __(
'user',
'Password is to short. Minimum {num}: characters.',
['num' => static::$module->passwordMinLength]
);
}
if (($this->scenario == self::SCENARIO_REGISTER || $this->scenario == self::SCENARIO_RESET) &&
$this->password != $this->password2) {
$this->errors['password2'] = __('user', 'Passwords are not the same.');
}
}
/**
* Get user role ids
*
* @return array An array containg role ids
*/
public function getRoleIds()
{
return Rbac::getUserRoleIds($this->id);
}
/**
* Activate an user
*
* @return boolean
*/
public function activate()
{
$this->confirmed_at = time();
return $this->save();
}
/**
* Check if the user is activated
* @return boolean
*/
public function isActivated()
{
return empty($this->confirmed_at) ? false : true;
}
/**
* Send Registration confirmation email
*
* @return boolean Return false if fail to send email
*/
public function sendRegistrationConfirmation(Router $router, SmtpMailer $mailer)
{
$siteName = getenv('SITE_NAME');
$baseUrl = $this->getAbsoluteBaseUrl();
$message = __('user', 'confirmation_mail_body', [
'site_name' => $siteName,
'link' => $baseUrl . $router->getUrl('user/default/confirmation', ['token' => $this->auth_key]),
'base_url' => $baseUrl,
'username' => $this->username,
]);
$subject = __('user', 'Registration confirmation on {site_name}', ['site_name' => $siteName]);
$mail = new Message();
$mail->setFrom($siteName . ' <' . getenv('NO_REPLY_EMAIL') . '>')
->addTo($this->email)
->setSubject($subject)
->setBody($message);
try {
$mailer->send($mail);
return true;
} catch (\Exception $e) {
$this->errors['sendmail'] = $e->getMessage();
}
return false;
}
/**
* Send reset password email
*
* @return boolean Return false if fail to send email
*/
public function sendResetPassword(Router $router, SmtpMailer $mailer)
{
$siteName = getenv('SITE_NAME');
$baseUrl = $this->getAbsoluteBaseUrl();
$message = __('user', 'reset_password_mail_body', [
'site_name' => $siteName,
'link' => $baseUrl . $router->getUrl('user/default/reset-password', ['token' => $this->auth_key]),
'username' => $this->username,
]);
$subject = __('user', 'Password change request on {site_name}', ['site_name' => $siteName]);
$mail = new Message();
$mail->setFrom($siteName . ' <' . getenv('NO_REPLY_EMAIL') . '>')
->addTo($this->email)
->setSubject($subject)
->setBody($message);
try {
$mailer->send($mail);
return true;
} catch (\Exception $e) {
$this->errors['sendmail'] = $e->getMessage();
}
return false;
}
/**
* Get users
*
* @param array $filters Array of filter conditions (['name' => ''])
* @param string $order The order condition
* @param number $start The offset start
* @param number $limit The offset limit
*
* @return array An array of user rows
*/
public static function find($filters = [], $order = '', $start = 0, $limit = 0)
{
$query = 'SELECT * FROM `user`';
$where = [];
if (!empty($filters['name'])) {
$where[] = '`name` LIKE :search';
}
if (!empty($where)) {
$query .= ' WHERE ' . implode(' AND ', $where);
}
$query .= ' ORDER BY ' . (empty($order) ? '`id` DESC' : $order);
if (!empty($start)) {
$query .= ' OFFSET ' . (int) $start;
}
if (!empty($limit)) {
$query .= ' LIMIT ' . (int) $limit;
}
$sth = static::$pdo->prepare($query);
$sth->execute($filters);
return $sth->fetchAll();
}
/**
* Find user by username
*
* @param string $username
* @return User|NULL
*/
public static function findByUsername($username)
{
$st = static::$pdo->prepare('SELECT id FROM user WHERE username = ?');
$st->bindParam(1, $username, \PDO::PARAM_STR);
if ($st->execute()) {
$id = $st->fetchColumn();
if ($id) {
$user = new static(static::$pdo);
$user->load($id);
return $user;
}
}
return null;
}
/**
* Find user by email
*
* @param string $email
* @return User|NULL
*/
public static function findByEmail($email)
{
$st = static::$pdo->prepare('SELECT id FROM user WHERE email = ?');
$st->bindParam(1, $email, \PDO::PARAM_STR);
if ($st->execute()) {
$id = $st->fetchColumn();
if ($id) {
$user = new static(static::$pdo);
return $user->load($id);
}
}
return null;
}
/**
* Find user by auth key
*
* @param string $token
* @return User|NULL
*/
public static function findByAuthKey($token)
{
$st = static::$pdo->prepare('SELECT id FROM `user` WHERE `auth_key` = ?');
if ($st->execute([$token])) {
$id = $st->fetchColumn();
if ($id) {
$user = new static(static::$pdo);
$user->load($id);
return $user;
}
}
return null;
}
/**
* Validate password
*
* @param string $password
* @return boolean
*/
public function validatePassword($password)
{
return $this->password == sha1($password);
}
/**
* Find user by Id
*
* @param int $id
* @return User|NULL
*/
public static function findIdentity($id)
{
try {
$user = new static(static::$pdo);
return $user->load($id);
} catch (\RuntimeException $e) {
}
return null;
}
/**
* {@inheritDoc}
* @see \piko\IdentityInterface::getId()
*/
public function getId()
{
return $this->id;
}
/**
* Get absolute base Url
*
* @return string
*/
protected function getAbsoluteBaseUrl()
{
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
return "$protocol://{$_SERVER['HTTP_HOST']}";
}
}

View File

@@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS `user` (
`id` int(11) NOT NULL auto_increment,
`name` varchar(50) NOT NULL,
`username` varchar(50) NOT NULL,
`email` varchar(100) NOT NULL,
`password` varchar(50) NOT NULL,
`auth_key` varchar(100) NOT NULL DEFAULT '',
`confirmed_at` datetime DEFAULT NULL,
`blocked_at` datetime DEFAULT NULL,
`registration_ip` varchar(40) DEFAULT '' COMMENT 'Stores ip v4 or ip v6',
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`last_login_at` datetime DEFAULT NULL,
`is_admin` tinyint(1) NOT NULL DEFAULT 0,
`timezone` varchar(40) DEFAULT '',
`profil` text NOT NULL DEFAULT '{}' COMMENT 'Json encoded profil',
PRIMARY KEY (`id`),
INDEX `username` (`username`),
UNIQUE INDEX `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,52 @@
CREATE TABLE IF NOT EXISTS "user" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
username TEXT NOT NULL,
email TEXT NOT NULL,
password TEXT NOT NULL,
auth_key TEXT,
confirmed_at INTEGER,
blocked_at INTEGER,
registration_ip TEXT,
created_at INTEGER,
updated_at INTEGER,
last_login_at INTEGER,
timezone TEXT,
profil TEXT,
UNIQUE(email)
);
CREATE TABLE IF NOT EXISTS auth_role
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(64) NOT NULL,
description TEXT,
UNIQUE(name)
);
CREATE TABLE IF NOT EXISTS auth_permission
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(64) NOT NULL,
UNIQUE(name)
);
CREATE TABLE IF NOT EXISTS auth_role_has_permission
(
role_id INTEGER NOT NULL,
permission_id INTEGER NOT NULL,
primary key (role_id, permission_id),
foreign key (role_id) references auth_role(id) on delete cascade on update cascade,
foreign key (permission_id) references auth_permission(id) on delete cascade on update cascade
);
CREATE TABLE IF NOT EXISTS auth_assignment
(
role_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
primary key (role_id, user_id),
foreign key (role_id) references auth_role(id) on delete cascade on update cascade,
foreign key (user_id) references "user" (id) on delete cascade on update cascade
);

View File

@@ -0,0 +1,58 @@
<?php
use function Piko\I18n\__;
assert($this instanceof Piko\View);
assert($user instanceof app\modules\user\models\User);
/* @var $message array */
/* @var $roles array */
$this->title = empty($user->id) ? __('user', 'Create user') : __('user', 'Edit user');
$roleIds = $user->getRoleIds();
?>
<div class="container">
<?php if (is_array($message)): ?>
<div class="alert alert-<?= $message['type'] ?> alert-dismissible" role="alert">
<?= $message['content'] ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif ?>
<form method="post">
<div class="mb-3">
<label for="name"><?= __('user', 'Name') ?></label>
<input type="text" class="form-control" id="name" name="name" value="<?= $user->name ?>">
</div>
<div class="mb-3">
<label for="email"><?= __('user', 'Email') ?></label>
<input type="text" class="form-control" id="email" name="email" value="<?= $user->email ?>">
</div>
<div class="mb-3">
<label for="username"><?= __('user', 'Username') ?></label>
<input type="text" class="form-control" id="username" name="username" value="<?= $user->username ?>">
</div>
<div class="mb-3">
<label for="password"><?= __('user', 'Password') ?></label>
<input type="text" class="form-control" id="password" name="password" value="">
</div>
<div class="mb-3">
<label for="roles"><?= __('user', 'Roles') ?></label>
<select class="form-select" id="roles" name="roles[]" multiple>
<?php foreach ($roles as $role): ?>
<option value="<?= $role['id']?>"<?= in_array($role['id'], $roleIds)? ' selected' : '' ?>><?= $role['name']?></option>
<?php endforeach ?>
</select>
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary"><?= __('user', 'Save') ?></button>
<a href="<?= $this->getUrl('user/admin/users')?>" class="btn btn-default"><?= __('user', 'Close') ?></a>
</div>
</form>
</div>

View File

@@ -0,0 +1,17 @@
<?php
use function Piko\I18n\__;
/* @var $this \piko\View */
/* @var $page string */
?>
<ul class="nav nav-tabs my-3">
<li class="nav-item">
<a class="nav-link<?= $page == 'users' ? ' active' : '' ?>" href="<?= $this->getUrl('user/admin/users') ?>"><?= __('user', 'Users') ?></a>
</li>
<li class="nav-item">
<a class="nav-link<?= $page == 'roles' ? ' active' : '' ?>" href="<?= $this->getUrl('user/admin/roles') ?>"><?= __('user', 'Roles') ?></a>
</li>
<li class="nav-item">
<a class="nav-link<?= $page == 'permissions' ? ' active' : '' ?>" href="<?= $this->getUrl('user/admin/permissions') ?>"><?= __('user', 'Permissions') ?></a>
</li>
</ul>

View File

@@ -0,0 +1,111 @@
<?php
use Piko;
use function Piko\I18n\__;
assert($this instanceof Piko\View);
/* @var $permissions array */
$this->title = __('user', 'Permissions');
$this->registerCSSFile(Piko::getAlias('@web/js/DataTables/datatables.min.css'));
$this->registerJsFile(Piko::getAlias('@web/js/jquery-3.7.1.min.js'));
$this->registerJsFile(Piko::getAlias('@web/js/DataTables/datatables.min.js'));
$script = <<<JS
$(function() {
$('#permissions-table').DataTable({
'order': [[1, 'desc']]
});
$('#delete').click(function(e) {
if (confirm('Êtes-vous sûr de vouloir effectuer cette action ?')) {
$('#admin-form').attr('action', '/user/admin/delete-permissions')
$('#admin-form').submit()
}
});
$('#btn-new-permission, .edit-permission').on('click', function(e) {
e.preventDefault();
var permissionName = '';
var permissionId = $(this).data('id');
var action = $(this).attr('href');
const modal = new bootstrap.Modal('#editPermissionModal');
if ($(this).hasClass('edit-permission')) {
permissionName = $(this).text();
}
$('#permission-name').val(permissionName);
modal.show();
$('#btn-save-permission').on('click', function() {
if ($('#permission-name').val()) {
$.ajax({
method: 'post',
url: action,
data: {name: $('#permission-name').val(), id: permissionId}
})
.done(function(data) {
if (data.status == 'success') {
location.reload();
}
if (data.status == 'error') {
alert(data.error)
}
});
}
});
});
});
JS;
$this->registerJs($script);
?>
<?= $this->render('nav', ['page' => 'permissions']) ?>
<form action="" method="post" id="admin-form">
<div class="btn-group mb-4" role="group">
<a href="<?= $this->getUrl('user/admin/edit-permission') ?>" class="btn btn-primary btn-sm" id="btn-new-permission"><?= __('user', 'New permission') ?></a>
<button type="button" class="btn btn-danger btn-sm" id="delete"><?= __('user', 'Delete') ?></button>
</div>
<table class="table table-striped" id="permissions-table">
<thead>
<tr>
<th><?= __('user', 'Name') ?></th>
<th><?= __('user', 'Id') ?></th>
</tr>
</thead>
<tbody>
<?php foreach($permissions as $permission): ?>
<tr>
<td>
<input type="checkbox" name="items[]" value="<?= $permission['id'] ?>">&nbsp;
<a href="<?= $this->getUrl('user/admin/edit-permission', ['id' => $permission['id']])?>"
class="edit-permission" data-id="<?= $permission['id'] ?>"><?= $permission['name'] ?></a>
</td>
<td><?= $permission['id'] ?></td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</form>
<div class="modal fade" id="editPermissionModal" tabindex="-1" role="dialog" aria-labelledby="editPermissionModal" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-body">
<input type="text" id="permission-name" class="form-control" placeholder="<?= __('user', 'Permission name') ?>">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= __('user', 'Cancel') ?></button>
<button type="button" class="btn btn-primary" id="btn-save-permission"><?= __('user', 'Save') ?></button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,152 @@
<?php
use Piko;
use function Piko\I18n\__;
assert($this instanceof Piko\View);
/* @var $roles array */
/* @var $permissions array */
$this->title = __('user', 'Roles');
$this->registerCSSFile(Piko::getAlias('@web/js/DataTables/datatables.min.css'));
$this->registerJsFile(Piko::getAlias('@web/js/jquery-3.7.1.min.js'));
$this->registerJsFile(Piko::getAlias('@web/js/DataTables/datatables.min.js'));
$confirmDeleteMsg = __('user', 'Are you sure you want to perform this action?');
$script = <<<JS
$(function() {
$('#roles-table').DataTable({
'order': [[1, 'desc']]
});
$('#delete').click(function(e) {
if (confirm('{$confirmDeleteMsg}')) {
$('#admin-form').attr('action', '/user/admin/delete-roles')
$('#admin-form').submit()
}
});
$('#btn-new, .edit-role').on('click', function(e) {
e.preventDefault();
var data = {
id: $(this).data('id'),
parent_id: $(this).data('parent_id'),
name: '',
description: $(this).data('description')
};
var action = $(this).attr('href');
const modal = new bootstrap.Modal('#editRoleModal');
if ($(this).hasClass('edit-role')) {
data.name = $(this).text();
}
$('#role-name').val(data.name);
$('#role-description').val(data.description);
$.ajax({
method: 'get',
url: action,
})
.done(function(data) {
if (data.role.permissions) {
$('#permissions').val(data.role.permissions)
}
});
modal.show();
$('#btn-save').on('click', function() {
if ($('#role-name').val()) {
data.name = $('#role-name').val();
data.description = $('#role-description').val();
data.parent_id = $('#role-parent-id').val();
data.permissions = $('#permissions').val();
$.ajax({
method: 'post',
url: action,
data: data
})
.done(function(data) {
if (data.status == 'success') {
location.reload();
}
});
}
});
});
});
JS;
$this->registerJs($script);
?>
<?= $this->render('nav', ['page' => 'roles']) ?>
<form action="" method="post" id="admin-form">
<div class="btn-group mb-4" role="group">
<a href="<?= $this->getUrl('user/admin/edit-role') ?>" class="btn btn-primary btn-sm" id="btn-new"><?= __('user', 'New role') ?></a>
<button type="button" class="btn btn-danger btn-sm" id="delete"><?= __('user', 'Delete') ?></button>
</div>
<table class="table table-striped" id="roles-table">
<thead>
<tr>
<th><?= __('user', 'Name') ?></th>
<th><?= __('user', 'Description') ?></th>
<th><?= __('user', 'Id') ?></th>
</tr>
</thead>
<tbody>
<?php foreach($roles as $role): ?>
<tr>
<td>
<input type="checkbox" name="items[]" value="<?= $role['id'] ?>">&nbsp;
<a href="<?= $this->getUrl('user/admin/edit-role', ['id' => $role['id']])?>"
class="edit-role"
data-description="<?= $role['description'] ?>"
data-id="<?= $role['id'] ?>"><?= $role['name'] ?></a>
</td>
<td><?= $role['description'] ?></td>
<td><?= $role['id'] ?></td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</form>
<div class="modal fade" id="editRoleModal" tabindex="-1" role="dialog" aria-labelledby="editRoleModal" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-body">
<div class="form-group">
<label for="role-name"><?= __('user', 'Role name') ?></label>
<input type="text" id="role-name" class="form-control">
</div>
<div class="form-group">
<label for="role-description"><?= __('user', 'Description') ?></label>
<textarea rows="3" class="form-control" id="role-description"></textarea>
</div>
<div class="form-group">
<label for="permissions"><?= __('user', 'Role permissions') ?></label>
<select class="form-select" id="permissions" multiple>
<?php foreach ($permissions as $perm): ?>
<option value="<?= $perm['id']?>"><?= $perm['name']?></option>
<?php endforeach ?>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= __('user', 'Cancel') ?></button>
<button type="button" class="btn btn-primary" id="btn-save"><?= __('user', 'Save') ?></button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,71 @@
<?php
use function Piko\I18n\__;
assert($this instanceof Piko\View);
/* @var $users array */
$this->title = __('user', 'Users management');
$this->registerCSSFile(Piko::getAlias('@web/js/DataTables/datatables.min.css'));
$this->registerJsFile(Piko::getAlias('@web/js/jquery-3.7.1.min.js'));
$this->registerJsFile(Piko::getAlias('@web/js/DataTables/datatables.min.js'));
$confirmDeleteMsg = __('user', 'Are you sure you want to perform this action?');
$script = <<<JS
$(document).ready(function() {
$('#users-table').DataTable({
'order': [[3, 'desc']]
});
$('#delete').click(function(e) {
if (confirm('{$confirmDeleteMsg}')) {
$('#admin-form').attr('action', '/user/admin/delete')
$('#admin-form').submit()
}
});
});
JS;
$this->registerJs($script);
?>
<?= $this->render('nav', ['page' => 'users']) ?>
<form action="" method="post" id="admin-form">
<div class="btn-group mb-4" role="group">
<a href="<?= $this->getUrl('user/admin/edit') ?>" class="btn btn-primary btn-sm"><?= __('user', 'Create user') ?></a>
<button type="button" class="btn btn-danger btn-sm" id="delete"><?= __('user', 'Delete') ?></button>
</div>
<table id="users-table" class="table table-striped">
<thead>
<tr>
<th><?= __('user', 'Name') ?></th>
<th><?= __('user', 'Username') ?></th>
<th><?= __('user', 'Email') ?></th>
<th><?= __('user', 'Last login at') ?></th>
<th><?= __('user', 'Created at') ?></th>
<th><?= __('user', 'Id') ?></th>
</tr>
</thead>
<tbody>
<?php foreach($users as $user): ?>
<tr>
<td>
<input type="checkbox" name="items[]" value="<?= $user['id'] ?>">&nbsp;
<a href="<?= $this->getUrl('user/admin/edit', ['id' => $user['id']])?>"><?= $user['name'] ?></a>
</td>
<td><?= $user['username']?></td>
<td><?= $user['email']?></td>
<td><?= empty($user['last_login_at']) ? '' : date('Y-m-d H:i', $user['last_login_at']) ?></td>
<td><?= date('Y-m-d H:i', $user['created_at']) ?></td>
<td><?= $user['id'] ?></td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</form>

View File

@@ -0,0 +1,93 @@
<?php
use function Piko\I18n\__;
assert($this instanceof Piko\View);
/* @var $message array */
/* @var $user piko\user\models\User */
$this->title = __('user', 'Edit your account');
if (is_array($message)) {
$this->params['message'] = $message;
}
if (!empty($user->profil)) {
$user->profil = json_decode($user->profil);
}
?>
<div class="container mt-5">
<h1><?= $this->title ?></h1>
<form method="post" novalidate>
<div class="form-group">
<label for="username"><?= __('user', 'Username') ?> : <strong><?= $user->username ?></strong></label>
</div>
<div class="form-group">
<label for="email"><?= __('user', 'Email') ?></label>
<input type="text" class="form-control" id="email" name="email" value="<?= $user->email ?>">
<?php if (!empty($user->errors['email'])): ?>
<div class="alert alert-danger" role="alert"><?= $user->errors['email'] ?></div>
<?php endif ?>
</div>
<div class="form-group">
<label for="password"><?= __('user', 'Password (leave blank to keep the same)') ?></label>
<input type="password" class="form-control" id="password" name="password" value="" autocomplete="off">
</div>
<div class="form-row">
<div class="col-md-6 mb-3">
<label for="lastname"><?= __('user', 'Last name') ?></label>
<input type="text" class="form-control" id="lastname" name="profil[lastname]" value="<?= isset($user->profil->lastname) ? $user->profil->lastname : '' ?>">
</div>
<div class="col-md-6 mb-3">
<label for="firstname"><?= __('user', 'First name') ?></label>
<input type="text" class="form-control" id="firstname" name="profil[firstname]" value="<?= isset($user->profil->firstname) ? $user->profil->firstname : '' ?>">
</div>
</div>
<div class="form-row">
<div class="col-md-6 mb-3">
<label for="company"><?= __('user', 'Company') ?></label>
<input type="text" class="form-control" id="company" name="profil[company]" value="<?= isset($user->profil->company) ? $user->profil->company : ''?>">
</div>
<div class="col-md-6 mb-3">
<label for="telephone"><?= __('user', 'Phone number') ?></label>
<input type="text" class="form-control" id="telephone" name="profil[telephone]" value="<?= isset($user->profil->telephone) ? $user->profil->telephone : ''?>">
</div>
</div>
<div class="form-group mb-3">
<label for="address"><?= __('user', 'Address') ?></label>
<input type="text" class="form-control" id="address" name="profil[address]" value="<?= isset($user->profil->address) ? $user->profil->address : ''?>">
</div>
<div class="form-row">
<div class="col-md-4 mb-3">
<label for="zipcode"><?= __('user', 'Zip code') ?></label>
<input type="text" class="form-control" id="zipcode" name="profil[zipcode]" value="<?= isset($user->profil->zipcode) ? $user->profil->zipcode : ''?>">
</div>
<div class="col-md-4 mb-3">
<label for="city"><?= __('user', 'City') ?></label>
<input type="text" class="form-control" id="city" name="profil[city]" value="<?= isset($user->profil->city) ? $user->profil->city : ''?>">
</div>
<div class="col-md-4 mb-3">
<label for="country"><?= __('user', 'Country') ?></label>
<input type="text" class="form-control" id="country" name="profil[country]" value="<?= isset($user->profil->country) ? $user->profil->country : ''?>">
</div>
</div>
<button type="submit" class="btn btn-primary"><?= __('user', 'Save') ?></button>
<a href="<?= Piko::getAlias('@web/')?>" class="btn btn-default"><?= __('user', 'Cancel') ?></a>
</form>
</div>

View File

@@ -0,0 +1,77 @@
<?php
use function Piko\I18n\__;
assert($this instanceof Piko\View);
/**
* @var $message boolean | array
* @var $canRegister boolean
*/
$this->title = __('user', 'Login');
$this->params['breadcrumbs'][] = $this->title;
if (is_array($message)) {
$this->params['message'] = $message;
}
?>
<main class="form-signin w-100 m-auto">
<form action="<?= $this->getUrl('user/default/login') ?>" id="login-form" method="post">
<h1 class="h3 mb-3 fw-normal"><?= $this->title ?></h1>
<div class="form-floating">
<input type="text" class="form-control" id="username" name="username" autofocus="autofocus" aria-required="true"
aria-invalid="true">
<label for="username"><?= __('user', 'Username') ?></label>
</div>
<div class="form-floating">
<input type="password" class="form-control" id="loginform-password" name="password" value="" aria-required="true">
<label for="loginform-password"><?= __('user', 'Password') ?></label>
</div>
<button class="btn btn-primary w-100 py-2" type="submit"><?= __('user', 'Login') ?></button>
<p class="mt-5 mb-3 text-body-secondary">&copy; 20172023</p>
<!--
<div class="form-group row">
<label class="col-sm-3 col-form-label" for="loginform-username"><?= __('user', 'Username') ?></label>
<div class="col-sm-9">
<input type="text" id="username" class="form-control" name="username" autofocus="autofocus" aria-required="true"
aria-invalid="true">
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label" for="loginform-password"><?= __('user', 'Password') ?></label>
<div class="col-sm-9">
<input type="password" id="loginform-password" class="form-control" name="password" value="" aria-required="true">
</div>
</div>
<div class="form-group">
<div class="offset-sm-3 col-sm-9">
<button type="submit" class="btn btn-primary" name="login-button"><?= __('user', 'Login') ?></button>
</div>
</div>
-->
</form>
<?php if ($canRegister): ?>
<div class="p-3 border bg-light text-dark">
<p><?= __('user', 'No account yet?') ?></p>
<p><a href="<?= $this->getUrl('user/default/register')?>" class="btn btn-primary"><?= __('user', 'Register') ?></a></p>
<hr>
<p><a href="<?= $this->getUrl('user/default/reminder')?>"><?= __('user', 'Forget password?') ?></a></p>
</div>
<?php endif ?>
</main>

View File

@@ -0,0 +1,82 @@
<?php
use piko\Piko;
/* @var $this \piko\View */
/* @var $router \piko\Router */
/* @var $message array */
$router = Piko::get('router');
$this->title = Piko::t('user', 'Register');
if (is_array($message)) {
$this->params['message'] = $message;
return;
}
$js = <<<SCRIPT
jQuery(document).ready(function($) {
function validateField(e) {
var that = this;
$.post('{$router->getUrl('user/default/check-registration')}', $('#register-form').serialize(), function(errors) {
if (errors[that.name]) {
$(that).addClass('is-invalid')
$(that).removeClass('is-valid')
$(that).next('.invalid-feedback').text(errors[that.name])
} else {
$(that).removeClass('is-invalid')
$(that).addClass('is-valid')
}
});
}
$('#username').focusout(validateField);
$('#email').focusout(validateField);
$('#password').focusout(validateField);
$('#password2').focusout(validateField);
});
SCRIPT;
$this->registerJs($js);
?>
<div class="container mt-5">
<h1><?= $this->title ?></h1>
<form method="post" id="register-form" novalidate>
<div class="form-group">
<label for="username"><?= Piko::t('user', 'Username') ?></label>
<input type="text" class="form-control" id="username" name="username" value="">
<div class="invalid-feedback"></div>
</div>
<div class="form-group">
<label for="email"><?= Piko::t('user', 'Email') ?></label>
<input type="text" class="form-control" id="email" name="email" value="">
<div class="invalid-feedback"></div>
</div>
<div class="form-group">
<label for="password"><?= Piko::t('user', 'Password') ?></label>
<input type="password" class="form-control" id="password" name="password" value="" autocomplete="off">
<div class="invalid-feedback"></div>
</div>
<div class="form-group">
<label for="password2"><?= Piko::t('user', 'Confirm your password') ?></label>
<input type="password" class="form-control" id="password2" name="password2" value="" autocomplete="off">
<div class="invalid-feedback"></div>
</div>
<button type="submit" class="btn btn-primary"><?= Piko::t('user', 'Register') ?></button>
</form>
</div>

View File

@@ -0,0 +1,31 @@
<?php
use piko\Piko;
/* @var $this \piko\View */
/* @var $message array */
/* @var $reminder string */
$this->title = Piko::t('user', 'Forget password');
if (is_array($message)) {
$this->params['message'] = $message;
}
?>
<div class="container" style="margin-top: 100px">
<h1><?= $this->title ?></h1>
<form method="post" id="reminder-form" novalidate>
<div class="form-group">
<label for="reminder"><?= Piko::t('user', 'Your email or your username') ?></label>
<input type="text" class="form-control" id="reminder" name="reminder" value="<?= $reminder ?>" autocomplete="off">
</div>
<button type="submit" class="btn btn-primary"><?= Piko::t('user', 'Send') ?></button>
</form>
</div>

View File

@@ -0,0 +1,75 @@
<?php
use piko\Piko;
/* @var $this \piko\View */
/* @var $user piko\user\models\User */
/* @var $message array */
/* @var $router \piko\Router */
$router = Piko::get('router');
$this->title = Piko::t('user', 'Change your account ({account}) password',['account' => $user->username]);
if (is_array($message)) {
$this->params['message'] = $message;
echo '<div class="container text-center"><a class="btn btn-primary" href="'. $router->getUrl('user/default/login').'">'
. Piko::t('user', 'Login') . '</a></div>';
return;
}
$js = <<<SCRIPT
jQuery(document).ready(function($) {
function validateField(e) {
var that = this;
$.post('{$router->getUrl('user/default/check-registration')}', $('#register-form').serialize(), function(errors) {
if (errors[that.name]) {
$(that).addClass('is-invalid')
$(that).removeClass('is-valid')
$(that).next('.invalid-feedback').text(errors[that.name])
} else {
$(that).removeClass('is-invalid')
$(that).addClass('is-valid')
}
});
}
$('#password').focusout(validateField);
$('#password2').focusout(validateField);
});
SCRIPT;
$this->registerJs($js);
?>
<div class="container" style="margin-top: 100px">
<h1 class="h4"><?= $this->title ?></h1>
<form method="post" id="register-form" novalidate>
<div class="form-group">
<label for="password"><?= Piko::t('user', 'Password') ?></label>
<input type="password" class="form-control" id="password" name="password" value="" autocomplete="off">
<div class="invalid-feedback"></div>
</div>
<div class="form-group">
<label for="password2"><?= Piko::t('user', 'Confirm your password') ?></label>
<input type="password" class="form-control" id="password2" name="password2" value="" autocomplete="off">
<div class="invalid-feedback"></div>
</div>
<button type="submit" class="btn btn-primary"><?= Piko::t('user', 'Send') ?></button>
</form>
</div>