Creating A Log In System - Part 4

The last part of creating a log in system is to create an admin page. We'd like to be able to add, change, and delete users. We'd also like to reset user passwords if someone forgets. We're going to build this out here!

We're using similar patterns here, but we're making it considerably more complex, including introducing more complex controller logic, filtering, and html. This is more of a real world example, with a more complete CSS design and html layout. We've also modernized our controllers with try..catch blocks for filtering and validating. We also have many different controller actions and model methods to round out this expansion.

The first step to this is creating three files: admin.php, adminmodels.php, and adminviews.php. We'll drop all of our new code in those files and this section will be complete!

adminviews.php

<?php
	
namespace AdminViews;

class AdminViews {
	// $users is a list of obfuscatedId, lastLogin, email, username, and isEnabled
	static public function index($users, $error="") {
		AdminViews::header();
		
		// list a table of users
		
		// show a table header
		?>
		
		<div class="user-list">

			<div class="container header">
				<div class="last-login">
					Last Login
				</div>
				<div class="user">
					User
				</div>
				<div class="email">
					Email
				</div>
				<div class="delete">
					Delete
				</div>
				<div class="disable">
					Disable
				</div>
				<div class="reset-password">
					Reset Password
				</div>
			</div>
			
			<?php
			
			// echo the results to the browser
			foreach ($users as $user) {
				// is disabled?
				$enabled = $user["isEnabled"] == true ? "" : "disabled";
			
				?>
					
					<div class="container <?= $enabled ?>">
						<div class="last-login">
							<?= date("m/d/Y", $user["lastLogin"]) ?>
						</div>
						<div class="user">
							<a href="admin.php?do=update&obfuscatedId=<?= $user["obfuscatedId"] ?>&csrf=<?= $_SESSION["csrf"] ?>">
								<?= $user["username"] ?>
							</a>
						</div>
						<div class="email">
							<a href="admin.php?do=update&obfuscatedId=<?= $user["obfuscatedId"] ?>&csrf=<?= $_SESSION["csrf"] ?>">
								<?= $user["email"] ?>
							</a>
						</div>
						<div class="delete">
							<a href="admin.php?do=delete&obfuscatedId=<?= $user["obfuscatedId"] ?>&csrf=<?= $_SESSION["csrf"] ?>">Delete</a>
						</div>
						<div class="disable">
							<a href="admin.php?do=disable&obfuscatedId=<?= $user["obfuscatedId"] ?>&csrf=<?= $_SESSION["csrf"] ?>">
								<?= $user["isEnabled"] == true ? "Disable" : "Enable"; ?>
							</a>
						</div>
						<div class="reset-password">
							<a href="admin.php?do=resetPassword&obfuscatedId=<?= $user["obfuscatedId"] ?>&csrf=<?= $_SESSION["csrf"] ?>">Reset Password</a>
						</div>
					</div>
					
				<?php
			}
		
		?>
		</div>
		<?php

		
		// create user form
		AdminViews::userEditForm(
			false, 
			[ 
				"username"=>isset($_POST["username"]) == true ? $_POST["username"] : "", 
				"email"=>isset($_POST["email"]) == true ? $_POST["email"] : "", 
			],
			$error
		);
		
		AdminViews::footer();
	}
	
	static public function update($userRecord, $error = "") {
		AdminViews::header();
		AdminViews::userEditForm(true, $userRecord, $error);
		AdminViews::footer();
	}
	
	static public function userEditForm($isUpdate = false, $userRecord = ["obfuscatedId"=>"", "username"=>"", "email"=>"", ], $error="") {
		
		?>
		<div class="create-user"><?= $isUpdate == true ? "Edit User" : "Create New User" ?></div>
		<form action="admin.php" method="post">
			
			<input type="hidden" name="csrf" value="<?= $_SESSION["csrf"] ?>">
			
			<input type="hidden" name="do" value="<?= $isUpdate == true ? "saveUserUpdate" : "saveNewUser" ?>">
			<?= $isUpdate == true ? '<input type="hidden" name="obfuscatedId" value="'.$userRecord["obfuscatedId"].'">' : '' ?>
			
			<label>Username</label>
			<div>
				<input type="text" name="username" value="<?= $userRecord["username"] ?>">
			</div>
			
			<label>Email</label>
			<div>
				<input type="text" name="email" value="<?= $userRecord["email"] ?>">
			</div>
			<input type="submit" value="Save">

			<?php
			// if there is an error, echo it
			if ( $error != "" ) {
				?>
				<div class="error">
					<?= $error ?>
				</div>
				<?php
			}
			?>
			
			
		</form>
		<?php
	}
	
	static public function header() {
		
?>
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
	<meta charset="utf-8">
	<title>Employees</title>
	<style>
		body{ background-color: #333; color: #ddd; }
		a{ color: #ddd; }
		.error{
			color: black;
			background-color: pink;
			margin-top: 10px;
			padding: 5px;
		}
		.body{
			margin: auto;
			width: 1000px;
		}
		.user-list{
			margin-bottom: 100px;
		}
		.container{
			display: grid;
			grid-template-columns: 150px 120px auto 100px 100px 120px;
			padding: 10px;
		}
		.header{
			background-color: rgba(255,255,255,0.1);
		}
		.disabled{
			color: gray;
		}
		.last-login{
			grid-column: 1;
		}
		.user{
			grid-column: 2;
		}
		.email{
			grid-column: 3;
		}
		.delete{
			grid-column: 4;
		}
		.disabled{
			grid-column: 5;
		}
		.reset-password{
			grid-column: 6;
		}
		.create-user{
			padding: 10px;
			background-color: rgba(255,255,255,0.1);
		}
	</style>
</head>
<body>
<div class="body">
	
<?php
	}
	
	static public function footer() {
?>
</div>
</body>
</html>
<?php
	}

}

admin.php

<?php
	
// require session to be loaded
require_once "session.php";

require_once "filter.php";
use \filter\filter;

require_once "adminmodels.php";
use \AdminModels\AdminModels;

require_once "adminviews.php";
use \AdminViews\AdminViews;

// echo '<pre>';
// print_r($_SESSION);
// echo '</pre>';

// echo '<pre>';
// print_r($_GET);
// echo '</pre>';

// if we're not logged in, send the user to login.php
if ( isset($_SESSION["loggedIn"]) == false || $_SESSION["loggedIn"] == false ) {
	header("Location: login.php");
	exit;
}


// handle GETS
if ( isset($_GET["do"]) == true ) {
	
	// check csrf token to make sure it matches
	// exit if any of these conditions are not met
	if (isset($_GET["csrf"]) == false || $_GET["csrf"] != $_SESSION["csrf"]) {
		exit;
	}
	
	// use $_GET here, because clicking a link and sending HTTP vars is a GET request
	if ($_GET["do"] == "update") {
		$obfuscatedId = 0;
		if (isset($_GET["obfuscatedId"]) == true && $_GET["obfuscatedId"] != "") {
			$obfuscatedId = $_GET["obfuscatedId"];
		}
		
		if ($obfuscatedId != "") {
			AdminViews::update( AdminModels::getUser($obfuscatedId) );
		}
		exit;
	}
	
	if ($_GET["do"] == "delete") {
		$obfuscatedId = "";
		if (isset($_GET["obfuscatedId"]) == true && $_GET["obfuscatedId"] != "") {
			$obfuscatedId = $_GET["obfuscatedId"];
		}
		
		if ($obfuscatedId != "") {
			AdminModels::deleteUser($obfuscatedId);
			header("Location: admin.php");
			exit;
		}
	}
	
	if ($_GET["do"] == "disable") {
		$obfuscatedId = "";
		if (isset($_GET["obfuscatedId"]) == true && $_GET["obfuscatedId"] != "") {
			$obfuscatedId = $_GET["obfuscatedId"];
		}
		
		if ($obfuscatedId != "") {
			AdminModels::disableUser($obfuscatedId);
			header("Location: admin.php");
			exit;
		}
	}
	
	if ($_GET["do"] == "resetPassword") {
		$obfuscatedId = "";
		if (isset($_GET["obfuscatedId"]) == true && $_GET["obfuscatedId"] != "") {
			$obfuscatedId = $_GET["obfuscatedId"];
		}
		
		if ($obfuscatedId != "") {
			AdminModels::resetPassword($obfuscatedId);
			header("Location: admin.php");
			exit;
		}
	}
}


// handle POSTS
if ( isset($_POST["do"]) == true ) {
	
	// check csrf token to make sure it matches
	// exit if any of these conditions are not met
	if (isset($_POST["csrf"]) == false || $_POST["csrf"] != $_SESSION["csrf"]) {
		exit;
	}
	
	if ($_POST["do"] == "saveNewUser") {
		
		try {
			// validate the username - will throw an exception if not good
			$username = filter::username($_POST["username"]);
			
			// validate the email - will throw an exception if not good
			$email = filter::email($_POST["email"]);
			
			// decide what IndexModels method we should use, depending on the request
			AdminModels::saveNewUser($username, $email);
			
			// after we've saved, forward the user back to our default screen
			header("Location: /admin.php");
			exit;
			
		} catch (\Exception $e) {
			
			// display an error, depending on the situation the user is in
			$results = AdminModels::index();
			AdminViews::index($results, $e->getMessage());
			
		}
		
	}
	
	// utilize this validation with saveUserUpdate
	if ($_POST["do"] == "saveUserUpdate") {
		
		try {
			// validate the username - will throw an exception if not good
			$username = filter::username($_POST["username"]);
			
			// validate the email - will throw an exception if not good
			$email = filter::email($_POST["email"]);
			
			// grab the nameId
			$obfuscatedId = "";
			if (isset($_POST["obfuscatedId"]) == true && $_POST["obfuscatedId"] != "") {
				$obfuscatedId = $_POST["obfuscatedId"];
			}
			
			if ($obfuscatedId != "") {
				AdminModels::saveUserUpdate($obfuscatedId, $username, $email);
			}
			
			// after we've saved, forward the user back to our default screen
			header("Location: /admin.php");
			exit;
			
		} catch (\Exception $e) {
			// show an error to the user
			if (isset($_POST["obfuscatedId"]) == true && $_POST["obfuscatedId"] != "") {
				AdminViews::update( AdminModels::getUser($_POST["obfuscatedId"]), $e->getMessage() );
			}
			
		}
		
	}
	
} else {
	// show the default screen, which is a list of employees
	$results = AdminModels::index();
	AdminViews::index($results);
}

adminmodels.php

<?php
	
namespace AdminModels;

require_once "db.php";
use \database\db;

class AdminModels {
	
	// default password when we're creating a user or resetting a password
	static private $defaultPassword = "[enter your default password here]";
	
	static public function index() {
		$db = new db;
		return $db->query("select obfuscatedId, unix_timestamp(lastLogin) as lastLogin, email, username, isEnabled from users where isDeleted = false order by username")->fetchAll();
	}
	
	static public function getUser($obfuscatedId) {
		$db = new db;
		return $db->query(
			"select obfuscatedId, email, username from users where obfuscatedId = :obfuscatedId",
			[ ":obfuscatedId"=>$obfuscatedId, ]
		)->fetchAll()[0];
	}
	
	static public function deleteUser($obfuscatedId) {
		$db = new db;
		$db->query(
			"update users set isDeleted = true where obfuscatedId = :obfuscatedId", 
			[ ":obfuscatedId"=>$obfuscatedId, ]
		);
	}
	
	static public function disableUser($obfuscatedId) {
		$db = new db;
		$db->query(
			"update users set isEnabled = !isEnabled where obfuscatedId = :obfuscatedId", 
			[ ":obfuscatedId"=>$obfuscatedId, ]
		);
	}
	
	static public function resetPassword($obfuscatedId) {
		
		// hash default password
		$passwordHash = password_hash(AdminModels::$defaultPassword, PASSWORD_DEFAULT);
		
		$db = new db;
		$db->query(
			"update users set passwordHash = :passwordHash, forcePasswordChange = true where obfuscatedId = :obfuscatedId", 
			[ ":passwordHash"=>$passwordHash, ":obfuscatedId"=>$obfuscatedId, ]
		);
	}
	
	static public function saveNewUser($username, $email) {
		
		// create new random obfuscatedId using PHP cryptographically secure random_bytes function
		// will generate a 16 byte string
		$obfuscatedId = bin2hex( random_bytes(8) );

		// hash default password
		$passwordHash = password_hash(AdminModels::$defaultPassword, PASSWORD_DEFAULT);
		
		$db = new db;
		$db->query(
			"insert into users set created = now(), lastLogin = now(), obfuscatedId = :obfuscatedId, username = :username, email = :email, passwordHash = :passwordHash",
			[ ":obfuscatedId"=>$obfuscatedId, ":username"=>$username, ":email"=>$email, ":passwordHash"=>$passwordHash, ]
		);
		
	}
	
	static public function saveUserUpdate($obfuscatedId, $username, $email) {
		$db = new db;
		$db->query(
			"update users set username = :username, email = :email where obfuscatedId = :obfuscatedId",
			[ ":obfuscatedId"=>$obfuscatedId, ":username"=>$username, ":email"=>$email, ]
		);
	}
}

Insert into filters.php

static public function email($email) {

	if ($email == "") {
		throw new \Exception("All fields are required");
	}
	
	// use php to determine if the email is in a valid email format
	if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
		throw new \Exception("Invalid email address");
	}

	// the email must be longer than nine chars, otherwise throw an error
	if (strlen($email) > 252) {
		throw new \Exception("Email address is too long");
	}
	
	$email = strtolower($email);
	
	return $email;
}