From 93b405ab9f69538546165c75a301c0c57a5359cf Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Tue, 26 Jul 2016 00:16:17 +0200 Subject: User authentication mechanism --- README.md | 9 +- classes/BusinessAdmin.php | 46 +++++++++ classes/constants.php | 5 +- classes/user.php | 172 +++++++++++++++++++++++++++++++ include/about.php | 3 +- include/assignments-edit.php | 1 + include/assignments-new.php | 3 +- include/assignments-overview.php | 2 + include/assignments-view.php | 2 + include/assignments.php | 3 +- include/clients-edit.php | 3 +- include/clients-new.php | 3 +- include/clients-overview.php | 4 +- include/clients-view.php | 2 + include/clients.php | 3 +- include/contacts-edit.php | 3 +- include/contacts-new.php | 3 +- include/contacts-overview.php | 4 +- include/contacts.php | 3 +- include/discounts-edit.php | 1 + include/discounts-new.php | 1 + include/discounts-overview.php | 2 + include/discounts.php | 1 + include/home.php | 5 +- include/offers-edit.php | 3 +- include/offers-new.php | 3 +- include/offers-overview.php | 2 + include/offers-view.php | 2 + include/offers.php | 3 +- include/settings.php | 84 +++++++++++++++ index.php | 1 + install/index.php | 19 ++++ install/upgrade.php | 13 +++ login-ajax.php | 37 +++++++ login.php | 89 ++++++++++++++++ nav.php | 215 ++++++++++++++++++++------------------- 36 files changed, 629 insertions(+), 126 deletions(-) create mode 100644 classes/user.php create mode 100644 include/settings.php create mode 100644 login-ajax.php create mode 100644 login.php diff --git a/README.md b/README.md index fdcfd8e..6c81c39 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,6 @@ git clone --recursive https://github.com/camilstaps/BusinessAdmin.git * Naturally, you should have your server configured to run PHP files with a PHP backend. -* BusinessAdmin does not have a user management system. Anyone who has access - to the URL, has access to the system and all its data. It's therefore - necessary that you implement HTTP basic authentication. Refer to the - documentation of your server for how to do that. - ## Create a database BusinessAdmin assumes a MySQL database is setup properly. This can be: @@ -161,6 +156,10 @@ are listed by name and removal time. This way, you never really lose your file. # Changelog +### 0.4 (Jul 26, 2016) + +0.4 User authentication mechanism + ### 0.3 (Jul 20, 2016) 0.3 Discounts diff --git a/classes/BusinessAdmin.php b/classes/BusinessAdmin.php index dc1f3e7..ce332ee 100644 --- a/classes/BusinessAdmin.php +++ b/classes/BusinessAdmin.php @@ -29,6 +29,52 @@ class BusinessAdmin { // Getters and setters //------------------------------------------------------------------------------ + /** + * Get all user ids + * + * @see BusinessAdmin::getUsers() This funtion returns instances of the user class instead of just the ids + * + * @param PDO $pdo The PDO class for database connection + * @param string[] $where An array of WHERE clauses that will be AND-ed into a prepared statement + * @param mixed[] $variables An array of variables that should go into the prepared statement + * + * @throws PDOException Is something went wrong with the database + * + * @return int[] The ids + */ + public static function getUserIds($pdo, $where = [], $variables = []) { + $ids = []; + $users = $pdo->prepare("SELECT `id` FROM `".constants::db_prefix."user`" . ((count($where) > 0) ? (" WHERE (" . implode(') AND (', $where) . ")") : "")); + $users->execute($variables); + $users = $users->fetchAll(PDO::FETCH_ASSOC); + foreach ($users as $user) { + $ids[] = $user['id']; + } + return $ids; + } + + /** + * Get all users + * + * @see BusinessAdmin::getUserIds() This function returns just the ids of the users, and not instances of the user class + * + * @param PDO $pdo The PDO class for database connection + * @param string[] $where An array of WHERE clauses that will be AND-ed into a prepared statement + * @param mixed[] $variables An array of variables that should go into the prepared statement + * + * @throws PDOException If something went wrong with the database + * + * @return user[] An array indexed by id of instances of the user class + */ + public static function getUsers($pdo, $where = [], $variables = []) { + $ids = self::getUserIds($pdo, $where, $variables); + $users = []; + foreach ($ids as $id) { + $users[$id] = new user($pdo, $id); + } + return $users; + } + /** * Get all client ids * diff --git a/classes/constants.php b/classes/constants.php index d16bde4..77f47b0 100644 --- a/classes/constants.php +++ b/classes/constants.php @@ -69,6 +69,9 @@ class constants { /** @const fa_valuta see http://fontawesome.io/icons/#currency; the fa- postfix for valuta */ const fa_valuta = 'eur'; + /** @const password_cost for the password_hash function. Run install?password_cost to benchmark your system */ + const password_cost = 10; + /** @const version Version of BusinessAdmin. Don't change this yourself! */ - const version = '0.3'; + const version = '0.4'; } diff --git a/classes/user.php b/classes/user.php new file mode 100644 index 0000000..261fa3d --- /dev/null +++ b/classes/user.php @@ -0,0 +1,172 @@ +. + */ + +/** + * An interface to the user table in the database + */ +class user { + /** + * @var pdo $pdo The PDO class for database communication + * @var int $id The id of the user + * @var string $username The username of the user + * @var string $password The (hashed) password of the user + */ + protected $pdo, $id, $username, $password; + + /** + * Hash a password + * + * @param string $password The password to be hashed + * @param int $cost The password cost + * + * @return string The hashed password + */ + public static function hash($password, $cost=null) { + return password_hash( + $password, + PASSWORD_DEFAULT, + ['cost' => is_null($cost) ? constants::password_cost : $cost] + ); + } + + /** + * Create a new instance + * + * @param PDO $pdo The PDO class, to access the database + * @param int $id The id of the user to fetch + * + * @throws PDOException If something went wrong with the database + * @throws Exception If the user could not be found + */ + public function __construct($pdo, $id) { + $this->pdo = $pdo; + + $stmt = $this->pdo->prepare("SELECT * FROM `".constants::db_prefix."user` WHERE `id`=?"); + $stmt->execute(array($id)); + if ($stmt->rowCount() == 0) { + throw new Exception("The user with id '$id' could not be found."); + } + $user = $stmt->fetch(PDO::FETCH_ASSOC); + + $this->id = $user['id']; + $this->username = $user['username']; + $this->password = $user['password']; + } + + //------------------------------------------------------------------------------ + // Getters and setters + //------------------------------------------------------------------------------ + + /** + * Get the ID of the user + * + * @return int The ID + */ + public function getId() { + return $this->id; + } + + /** + * Get the username of the user + * + * @return string The username + */ + public function getUsername() { + return $this->username; + } + + /** + * Set the username of the user + * + * @param string $username The new username for the user + * + * @throws PDOException If something went wrong with the database + * + * @return bool True on succes, false on failure + */ + public function setName($username) { + $stmt = $this->pdo->prepare("UPDATE `".constants::db_prefix."user` SET `username`=? WHERE `id`=?"); + $stmt->execute(array($username, $this->id)); + if ($stmt->rowCount() == 1) { + $this->username = $username; + return true; + } else { + return false; + } + } + + /** + * Set the password of the user + * + * @param string $password The new password for the user + * + * @throws PDOException If something went wrong with the database + * + * @return bool True on succes, false on failure + */ + public function setPassword($password) { + $password = self::hash($password); + $stmt = $this->pdo->prepare("UPDATE `".constants::db_prefix."user` SET `password`=? WHERE `id`=?"); + $stmt->execute(array($password, $this->id)); + if ($stmt->rowCount() == 1) { + $this->password = $password; + return true; + } else { + return false; + } + } + + //------------------------------------------------------------------------------ + // Other functions + //------------------------------------------------------------------------------ + + /** + * Verify a password + * + * @param string $password The password to verify + * + * @return bool True iff the password can be accepted + */ + public function verifyPassword($password) { + return password_verify($password, $this->password); + } + + /** + * Remove this user from the database + * + * If this doesn't succeed (i.e. false is returned), that means the user was removed manually or by another instance of this class + * + * @throws PDOException If something went wrong with the database + * + * @return bool True on success, false on failure + */ + public function delete() { + $stmt = $this->pdo->prepare("DELETE FROM `".constants::db_prefix."user` WHERE `id`=?"); + $stmt->execute(array($this->id)); + if ($stmt->rowCount() != 1) { + return false; + } else { + return true; + } + } +} diff --git a/include/about.php b/include/about.php index bc93e14..554a80f 100644 --- a/include/about.php +++ b/include/about.php @@ -18,6 +18,7 @@ */ require_once('./index.php'); +require_once('./login.php'); require('./header.php'); ?> @@ -104,4 +105,4 @@ info@camilstaps.nl \ No newline at end of file +?> diff --git a/include/assignments-edit.php b/include/assignments-edit.php index 4faad64..b52311a 100644 --- a/include/assignments-edit.php +++ b/include/assignments-edit.php @@ -18,6 +18,7 @@ */ require_once('./conf.php'); +require_once('./login-ajax.php'); $response = new response(); diff --git a/include/assignments-new.php b/include/assignments-new.php index 2de3b1f..7898a42 100644 --- a/include/assignments-new.php +++ b/include/assignments-new.php @@ -18,6 +18,7 @@ */ require_once('./conf.php'); +require_once('./login-ajax.php'); $response = new response(); @@ -42,4 +43,4 @@ try { $response->success = false; $response->message = "The assignment could not be created due to an error."; } -echo $response->getJson(); \ No newline at end of file +echo $response->getJson(); diff --git a/include/assignments-overview.php b/include/assignments-overview.php index 903a772..2de6858 100644 --- a/include/assignments-overview.php +++ b/include/assignments-overview.php @@ -16,6 +16,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + +require_once('./login.php'); ?>
diff --git a/include/assignments-view.php b/include/assignments-view.php index ffdc507..7a2923e 100644 --- a/include/assignments-view.php +++ b/include/assignments-view.php @@ -17,6 +17,8 @@ * along with this program. If not, see . */ +require_once('./login.php'); + $_assignment = new assignment($_pdo, $_id); ?>
diff --git a/include/assignments.php b/include/assignments.php index 5ba21c6..eaa7163 100644 --- a/include/assignments.php +++ b/include/assignments.php @@ -18,6 +18,7 @@ */ require_once('./index.php'); +require_once('./login.php'); require('./header.php'); ?> @@ -95,4 +96,4 @@ require('./header.php'); \ No newline at end of file +?> diff --git a/include/clients-edit.php b/include/clients-edit.php index c0b83c8..7d8d6fa 100644 --- a/include/clients-edit.php +++ b/include/clients-edit.php @@ -18,6 +18,7 @@ */ require_once('./conf.php'); +require_once('./login-ajax.php'); $response = new response(); @@ -39,4 +40,4 @@ try { $response->success = false; $response->message = "The client could not be edited due to an exception."; } -echo $response->message; \ No newline at end of file +echo $response->message; diff --git a/include/clients-new.php b/include/clients-new.php index 9466638..b073b8e 100644 --- a/include/clients-new.php +++ b/include/clients-new.php @@ -18,6 +18,7 @@ */ require_once('./conf.php'); +require_once('./login-ajax.php'); $response = new response(); @@ -35,4 +36,4 @@ try { $response->success = false; $response->message = "The client could not be created due to a PDO error ({$e->getMessage()})."; } -echo $response->getJson(); \ No newline at end of file +echo $response->getJson(); diff --git a/include/clients-overview.php b/include/clients-overview.php index f3e2a24..7ce45a6 100644 --- a/include/clients-overview.php +++ b/include/clients-overview.php @@ -16,6 +16,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + +require_once('./login.php'); ?>
@@ -117,4 +119,4 @@
-
\ No newline at end of file + diff --git a/include/clients-view.php b/include/clients-view.php index 6aa900c..101d530 100644 --- a/include/clients-view.php +++ b/include/clients-view.php @@ -17,6 +17,8 @@ * along with this program. If not, see . */ +require_once('./login.php'); + $_client = new client($_pdo, $_id); ?>
diff --git a/include/clients.php b/include/clients.php index 7248e0c..88608ab 100644 --- a/include/clients.php +++ b/include/clients.php @@ -18,6 +18,7 @@ */ require_once('./index.php'); +require_once('./login.php'); require('./header.php'); ?> @@ -95,4 +96,4 @@ require('./header.php'); \ No newline at end of file +?> diff --git a/include/contacts-edit.php b/include/contacts-edit.php index 9e7c606..a2101b2 100644 --- a/include/contacts-edit.php +++ b/include/contacts-edit.php @@ -18,6 +18,7 @@ */ require_once('./conf.php'); +require_once('./login-ajax.php'); $response = new response(); @@ -62,4 +63,4 @@ try { $response->success = false; $response->message = "The contact could not be edited due to an exception."; } -echo $response->message; \ No newline at end of file +echo $response->message; diff --git a/include/contacts-new.php b/include/contacts-new.php index c04fa72..0a72afb 100644 --- a/include/contacts-new.php +++ b/include/contacts-new.php @@ -18,6 +18,7 @@ */ require_once('./conf.php'); +require_once('./login-ajax.php'); $response = new response(); @@ -44,4 +45,4 @@ try { $response->success = false; $response->message = "The contact could not be created due to an error."; } -echo $response->getJson(); \ No newline at end of file +echo $response->getJson(); diff --git a/include/contacts-overview.php b/include/contacts-overview.php index cd3b1bc..cd7fc47 100644 --- a/include/contacts-overview.php +++ b/include/contacts-overview.php @@ -16,6 +16,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + +require_once('./login.php'); ?>
@@ -159,4 +161,4 @@
- \ No newline at end of file + diff --git a/include/contacts.php b/include/contacts.php index 787bd9a..4b35603 100644 --- a/include/contacts.php +++ b/include/contacts.php @@ -18,6 +18,7 @@ */ require_once('./index.php'); +require_once('./login.php'); require('./header.php'); ?> @@ -95,4 +96,4 @@ require('./header.php'); \ No newline at end of file +?> diff --git a/include/discounts-edit.php b/include/discounts-edit.php index e6859a1..a760e9c 100644 --- a/include/discounts-edit.php +++ b/include/discounts-edit.php @@ -18,6 +18,7 @@ */ require_once('./conf.php'); +require_once('./login-ajax.php'); $response = new response(); diff --git a/include/discounts-new.php b/include/discounts-new.php index 1900d47..8a5f527 100644 --- a/include/discounts-new.php +++ b/include/discounts-new.php @@ -18,6 +18,7 @@ */ require_once('./conf.php'); +require_once('./login-ajax.php'); $response = new response(); diff --git a/include/discounts-overview.php b/include/discounts-overview.php index 6160a8c..f9a3630 100644 --- a/include/discounts-overview.php +++ b/include/discounts-overview.php @@ -16,6 +16,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + +require_once('./login.php'); ?>
diff --git a/include/discounts.php b/include/discounts.php index 83cb1b8..8b160ab 100644 --- a/include/discounts.php +++ b/include/discounts.php @@ -18,6 +18,7 @@ */ require_once('./index.php'); +require_once('./login.php'); require('./header.php'); ?> diff --git a/include/home.php b/include/home.php index 644f2ae..cb624fe 100644 --- a/include/home.php +++ b/include/home.php @@ -17,8 +17,9 @@ * along with this program. If not, see . */ -require_once('index.php'); -require('header.php'); +require_once('./index.php'); +require_once('./login.php'); +require('./header.php'); ?>
diff --git a/include/offers-edit.php b/include/offers-edit.php index b2a7156..95de9b3 100644 --- a/include/offers-edit.php +++ b/include/offers-edit.php @@ -18,6 +18,7 @@ */ require_once('./conf.php'); +require_once('./login-ajax.php'); $response = new response(); @@ -56,4 +57,4 @@ try { $response->success = false; $response->message = "The offer could not be edited due to an exception."; } -echo $response->message; \ No newline at end of file +echo $response->message; diff --git a/include/offers-new.php b/include/offers-new.php index 46bec3b..0d86293 100644 --- a/include/offers-new.php +++ b/include/offers-new.php @@ -18,6 +18,7 @@ */ require_once('./conf.php'); +require_once('./login-ajax.php'); $response = new response(); @@ -36,4 +37,4 @@ try { $response->success = false; $response->message = "The offer could not be created due to an error."; } -echo $response->getJson(); \ No newline at end of file +echo $response->getJson(); diff --git a/include/offers-overview.php b/include/offers-overview.php index 594f01c..7fd2de2 100644 --- a/include/offers-overview.php +++ b/include/offers-overview.php @@ -16,6 +16,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + +require_once('./login.php'); ?>
diff --git a/include/offers-view.php b/include/offers-view.php index eec7701..082af35 100644 --- a/include/offers-view.php +++ b/include/offers-view.php @@ -17,6 +17,8 @@ * along with this program. If not, see . */ +require_once('./login.php'); + $_offer = new offer($_pdo, $_id); ?>
diff --git a/include/offers.php b/include/offers.php index 872773d..1aa871a 100644 --- a/include/offers.php +++ b/include/offers.php @@ -18,6 +18,7 @@ */ require_once('./index.php'); +require_once('./login.php'); require('./header.php'); ?> @@ -154,4 +155,4 @@ require('./header.php'); \ No newline at end of file +?> diff --git a/include/settings.php b/include/settings.php new file mode 100644 index 0000000..7dfbbc3 --- /dev/null +++ b/include/settings.php @@ -0,0 +1,84 @@ +. + */ + +require_once('./index.php'); +require_once('./login.php'); +require('./header.php'); +?> + +
+ + + + +
+
+
+

Settings

+
+ +
+ +
+
+
+
Password
+
+ The passwords don\'t match.
'; + } else if (!$_user->verifyPassword($_POST['password_current'])) { + echo '
The current password was incorrect.
'; + } else { + try { + $_user->setPassword($_POST['password_update']); + echo '
Password successfully changed.
'; + } catch (PDOException $e) { + echo '
An unknown error occurred.
'; + } + } + } + ?> +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+
+ +
+ + +
+ + diff --git a/index.php b/index.php index 129c83c..3c8d842 100644 --- a/index.php +++ b/index.php @@ -53,6 +53,7 @@ $pages = array( '/discounts/new' => './include/discounts-new.php', '/discounts/edit' => './include/discounts-edit.php', '/about' => './include/about.php', + '/settings' => './include/settings.php', '/ajax/collapse' => './include/ajax-collapse.php' ); diff --git a/install/index.php b/install/index.php index 6c57769..41fb450 100644 --- a/install/index.php +++ b/install/index.php @@ -89,6 +89,13 @@ if (isset($_GET['create_tables'])) { KEY `contactId_2` (`contactId`), KEY `invoice_fileId_2` (`invoice_fileId`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1"); + + $_pdo->query("CREATE TABLE IF NOT EXISTS `".constants::db_prefix."user` ( + `id` smallint(5) unsigned NOT NULL AUTO_INCREMENT, + `username` varchar(24) NOT NULL, + `password` varchar(255) NOT NULL, + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=latin1;"); $_pdo->query("ALTER TABLE `".constants::db_prefix."assignment` ADD CONSTRAINT `assignment_ibfk_1` FOREIGN KEY (`offerId`) REFERENCES `".constants::db_prefix."offer` (`id`)"); @@ -120,6 +127,17 @@ if (isset($_GET['create_folders'])) { echo "Creating folder `" . constants::files_folder_trash . "` failed.
"; } } + +if (isset($_GET['password_cost'])) { + $target = 1; + $start = $end = 0; + for ($cost = 10; $end - $start < $target; $cost++) { + $start = microtime(true); + user::hash('test', $cost); + $end = microtime(true); + } + echo "Password cost suggestion: $cost.
You can set this in classes/constants.php."; +} ?>
@@ -129,6 +147,7 @@ if (isset($_GET['create_folders'])) {
  1. Create database tables
  2. Create folders
  3. +
  4. Finding a good password cost

When you're done, it would be the neatest to remove the /install folder (even though this whole control panel should not be accessible for the public).

diff --git a/install/upgrade.php b/install/upgrade.php index 04cc03a..e145ba7 100644 --- a/install/upgrade.php +++ b/install/upgrade.php @@ -73,6 +73,19 @@ if (isset($_GET['upgrade'])) { } } + if (lower_version($_GET['upgrade'], '0.4')) { + try { + $_pdo->query("CREATE TABLE IF NOT EXISTS `".constants::db_prefix."user` ( + `id` smallint(5) unsigned NOT NULL AUTO_INCREMENT, + `username` varchar(24) NOT NULL, + `password` varchar(255) NOT NULL, + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=latin1;"); + } catch (PDOException $e) { + echo "Altering the database structure failed with a PDOException ({$e->getCode()}): {$e->getMessage()}
" . $e->getTraceAsString(); + } + } + echo "
All done."; } ?> diff --git a/login-ajax.php b/login-ajax.php new file mode 100644 index 0000000..f8e1424 --- /dev/null +++ b/login-ajax.php @@ -0,0 +1,37 @@ +. + */ + +require_once('./conf.php'); + +if (!isset($_SESSION['login']) || $_SESSION['login'] === false) { + print(json_encode(['error' => 'You need to be logged in.'])); + die(); +} + +$_user = new user($_pdo, $_SESSION['login']); diff --git a/login.php b/login.php new file mode 100644 index 0000000..e60b7ed --- /dev/null +++ b/login.php @@ -0,0 +1,89 @@ +. + */ + +require_once('./conf.php'); + +if (isset($_GET['logout'])) { + $_SESSION['login'] = false; + header('Location: ' . constants::url_external); + die(); +} + +if (!isset($_SESSION['login']) || $_SESSION['login'] === false) { + if (isset($_POST['username'])) { + $users = BusinessAdmin::getUsers($_pdo, ['`username`=?'], [$_POST['username']]); + if (count($users) == 0) { + $_msg = "No user {$_POST['username']} found.
"; + } else { + $user = array_pop($users); + if ($user->verifyPassword($_POST['password'])) { + $_SESSION['login'] = $user->getId(); + $_user = $user; + return; + } else { + $_msg = "Password incorrect.
"; + } + } + } + + include('./header.php'); +?> +
+
+
+ +
+
+
+
+ -- cgit v1.2.3