diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..48b8bf9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+vendor/
diff --git a/app/config/config.php b/app/config/config.php
new file mode 100644
index 0000000..22c03f3
--- /dev/null
+++ b/app/config/config.php
@@ -0,0 +1,20 @@
+
diff --git a/app/controller/UserController.php b/app/controller/UserController.php
new file mode 100644
index 0000000..3e537aa
--- /dev/null
+++ b/app/controller/UserController.php
@@ -0,0 +1,36 @@
+password)) {
+ $user->login_attempts = 0;
+ $user->save();
+ return $user;
+ } else {
+ $user->login_attempts++;
+ $user->save();
+ return null;
+ }
+ }
+
+ public static function create($request) {
+ $username = htmlspecialchars($request['username']);
+ $password = htmlspecialchars($request['password']);
+ $user = new User;
+ $user->username = $username;
+ $user->password = password_hash($password, PASSWORD_DEFAULT);
+ $user->save();
+ }
+}
+?>
diff --git a/app/database/Database.php b/app/database/Database.php
new file mode 100644
index 0000000..e04e87e
--- /dev/null
+++ b/app/database/Database.php
@@ -0,0 +1,10 @@
+
diff --git a/app/database/init.php b/app/database/init.php
new file mode 100644
index 0000000..f523308
--- /dev/null
+++ b/app/database/init.php
@@ -0,0 +1,38 @@
+exec('CREATE DATABASE IF NOT EXISTS ' . Config::db_name);
+
+/* Connect to database */
+$dbh = Database::connect();
+
+/* Create users table if it doesn't exist */
+$stmt = 'CREATE TABLE IF NOT EXISTS users (
+ id INT(6) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ username VARCHAR(32) NOT NULL,
+ password VARCHAR(64) NOT NULL,
+ access VARCHAR(8) NOT NULL,
+ login_attempts INT(6) UNSIGNED NOT NULL
+)';
+$dbh->exec($stmt);
+
+/* Create user and admin if they don't exist */
+$user = new User;
+$user->username = 'user';
+$user->password = password_hash('userpass', PASSWORD_DEFAULT);
+$user->access = 'user';
+$user->save();
+
+$user = new User;
+$user->username = 'admin';
+$user->password = password_hash('adminpass', PASSWORD_DEFAULT);
+$user->access = 'admin';
+$user->save();
+
+?>
diff --git a/app/forms/change_password.php b/app/forms/change_password.php
new file mode 100644
index 0000000..aa1190d
--- /dev/null
+++ b/app/forms/change_password.php
@@ -0,0 +1,41 @@
+
+
+
diff --git a/app/forms/create.php b/app/forms/create.php
new file mode 100644
index 0000000..2acc73d
--- /dev/null
+++ b/app/forms/create.php
@@ -0,0 +1,39 @@
+
+
+
diff --git a/app/forms/edit.php b/app/forms/edit.php
new file mode 100644
index 0000000..0b50ace
--- /dev/null
+++ b/app/forms/edit.php
@@ -0,0 +1,40 @@
+
+
diff --git a/app/forms/login.php b/app/forms/login.php
new file mode 100644
index 0000000..80be14f
--- /dev/null
+++ b/app/forms/login.php
@@ -0,0 +1,43 @@
+
+
+
diff --git a/app/include/http.php b/app/include/http.php
new file mode 100644
index 0000000..e3696e2
--- /dev/null
+++ b/app/include/http.php
@@ -0,0 +1,24 @@
+
diff --git a/app/model/User.php b/app/model/User.php
new file mode 100644
index 0000000..606b228
--- /dev/null
+++ b/app/model/User.php
@@ -0,0 +1,106 @@
+username)) {
+ $stmt = $dbh->prepare("UPDATE users SET password=:password, access=:access, login_attempts=:attempts WHERE username=:username");
+ } else {
+ $stmt = $dbh->prepare("INSERT INTO users (username, password, access, login_attempts) VALUES (:username, :password, :access, :attempts)");
+ }
+ $stmt->bindParam(':username', $this->username);
+ $stmt->bindParam(':password', $this->password);
+ $stmt->bindParam(':access', $this->access);
+ $stmt->bindParam(':attempts', $this->login_attempts);
+ $stmt->execute();
+ $this->id = $dbh->lastInsertId();
+ }
+
+ public function delete() {
+ $dbh = Database::connect();
+ $stmt = $dbh->prepare("DELETE FROM users WHERE id=:id");
+ $stmt->bindParam(':id', $this->id);
+ $stmt->execute();
+ }
+
+ public static function exists($username) {
+ $dbh = Database::connect();
+ $stmt = $dbh->prepare("SELECT count(*) FROM users WHERE username = :username");
+ $stmt->bindParam(':username', $username);
+ $stmt->execute();
+ return $stmt->fetchColumn() > 0;
+ }
+
+ public static function get($username) {
+ $users = User::all();
+ foreach ($users as $user) {
+ if ($user->username == $username) {
+ return $user;
+ }
+ }
+ return null;
+ }
+
+ public static function all() {
+ $dbh = Database::connect();
+ $stmt = $dbh->prepare("SELECT * from users");
+ $stmt->execute();
+ return array_map(function ($row) {
+ $user = new User;
+ $user->id = $row['id'];
+ $user->username = $row['username'];
+ $user->password = $row['password'];
+ $user->access = $row['access'];
+ $user->login_attempts = $row['login_attempts'];
+ return $user;
+ }, $stmt->fetchAll());
+ }
+
+ public static function authenticated() {
+ try {
+ $jwt = Http::cookie(Config::cookie_name);
+ if (!$jwt) return null;
+ $token = JWT::decode($jwt, base64_encode(Config::secret_key), ['HS512']);
+ return $token->data;
+ } catch (Exception $e) {
+ Http::remove_cookie(Config::cookie_name);
+ return null;
+ }
+ }
+
+ public function token() {
+ $data = [
+ 'iat' => time(),
+ 'jti' => base64_encode(random_bytes(32)),
+ 'iss' => Config::server_name,
+ 'nbf' => time(),
+ 'exp' => time() + (7 * 24 * 60 * 60),
+ 'data' => [
+ 'userid' => $this->id,
+ 'username' => $this->username,
+ 'access' => $this->access,
+ ],
+ ];
+
+ $key = base64_encode(Config::secret_key);
+
+ return JWT::encode($data, $key, 'HS512');
+ }
+}
+?>
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..9af63b0
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,5 @@
+{
+ "require": {
+ "firebase/php-jwt": "^4.0"
+ }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..3dc6e22
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,61 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "97857c969035c5b9de48fb918b6ceca2",
+ "packages": [
+ {
+ "name": "firebase/php-jwt",
+ "version": "v4.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/firebase/php-jwt.git",
+ "reference": "dccf163dc8ed7ed6a00afc06c51ee5186a428d35"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/firebase/php-jwt/zipball/dccf163dc8ed7ed6a00afc06c51ee5186a428d35",
+ "reference": "dccf163dc8ed7ed6a00afc06c51ee5186a428d35",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Firebase\\JWT\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Neuman Vong",
+ "email": "neuman+pear@twilio.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Anant Narayanan",
+ "email": "anant@php.net",
+ "role": "Developer"
+ }
+ ],
+ "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
+ "homepage": "https://github.com/firebase/php-jwt",
+ "time": "2016-07-18T04:51:16+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": []
+}
diff --git a/public/account.php b/public/account.php
new file mode 100644
index 0000000..5da3ac2
--- /dev/null
+++ b/public/account.php
@@ -0,0 +1,14 @@
+
diff --git a/public/admin.php b/public/admin.php
new file mode 100644
index 0000000..23229dd
--- /dev/null
+++ b/public/admin.php
@@ -0,0 +1,73 @@
+access != 'admin') {
+ Http::redirect('index.php');
+}
+
+$users = User::all();
+
+include 'template/header.html';
+include 'template/user_menu_button.php';
+?>
+
+
+
+
+
+
+
+ perm_identity
+ username ?>
+
+
+ verified_user
+ Access level: access ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/change_password.php b/public/change_password.php
new file mode 100644
index 0000000..5df27b2
--- /dev/null
+++ b/public/change_password.php
@@ -0,0 +1,21 @@
+ $data->username, 'password' => $_POST['password']];
+$user = UserController::authenticate($info);
+
+if (!$user) {
+ Http::redirect('account.php', ['error' => '1']);
+}
+
+$user->password = password_hash($_POST['new_password'], PASSWORD_DEFAULT);
+$user->save();
+
+Http::redirect('index.php');
diff --git a/public/create.php b/public/create.php
new file mode 100644
index 0000000..bf30f96
--- /dev/null
+++ b/public/create.php
@@ -0,0 +1,20 @@
+access != 'admin') {
+ Http::redirect('index.php');
+}
+
+/* TODO: Validate input */
+$params = Http::post_params();
+$user = new User;
+$user->username = $params['username'];
+$user->password = password_hash($params['password'], PASSWORD_DEFAULT);
+$user->access = $params['access'];
+$user->save();
+
+Http::redirect('admin.php');
+?>
diff --git a/public/css/admin.css b/public/css/admin.css
new file mode 100644
index 0000000..3b7ee42
--- /dev/null
+++ b/public/css/admin.css
@@ -0,0 +1,15 @@
+#create-modal {
+ background: white;
+}
+
+span.username {
+ padding-left: 10px;
+}
+
+.admin-container {
+ padding-top: 50px;
+}
+
+.edit-modal {
+ position: absolute;
+}
diff --git a/public/css/app.css b/public/css/app.css
new file mode 100644
index 0000000..6bc964c
--- /dev/null
+++ b/public/css/app.css
@@ -0,0 +1,17 @@
+@import url("login.css");
+@import url("admin.css");
+@import url("user.css");
+
+
+body {
+ background-color: #fcfcfc !important;
+}
+
+main {
+ height: 100vh;
+ width: 100% !important;
+}
+
+input:-webkit-autofill {
+ -webkit-box-shadow: 0 0 0 1000px white inset !important;
+}
diff --git a/public/css/login.css b/public/css/login.css
new file mode 100644
index 0000000..193d8b4
--- /dev/null
+++ b/public/css/login.css
@@ -0,0 +1,13 @@
+.login-header {
+ padding: 20px;
+}
+
+.login-container {
+ height: 100%;
+ margin: 0 !important;
+}
+
+.error-message {
+ color: red;
+ margin: 15px 0 0;
+}
diff --git a/public/css/user.css b/public/css/user.css
new file mode 100644
index 0000000..b738705
--- /dev/null
+++ b/public/css/user.css
@@ -0,0 +1,9 @@
+.user-button {
+ position: absolute;
+ top: 5px !important;
+ right: 10px !important;
+}
+
+.user-container {
+ padding-top: 50px;
+}
diff --git a/public/delete.php b/public/delete.php
new file mode 100644
index 0000000..bf67e8c
--- /dev/null
+++ b/public/delete.php
@@ -0,0 +1,19 @@
+access != 'admin') {
+ Http::redirect('index.php');
+}
+
+$username = Http::post_params()['username'];
+$user = User::get($username);
+
+if ($user) {
+ $user->delete();
+}
+
+Http::redirect('admin.php');
+?>
diff --git a/public/edit.php b/public/edit.php
new file mode 100644
index 0000000..bf30f96
--- /dev/null
+++ b/public/edit.php
@@ -0,0 +1,20 @@
+access != 'admin') {
+ Http::redirect('index.php');
+}
+
+/* TODO: Validate input */
+$params = Http::post_params();
+$user = new User;
+$user->username = $params['username'];
+$user->password = password_hash($params['password'], PASSWORD_DEFAULT);
+$user->access = $params['access'];
+$user->save();
+
+Http::redirect('admin.php');
+?>
diff --git a/public/img/background.png b/public/img/background.png
new file mode 100644
index 0000000..bc382e0
Binary files /dev/null and b/public/img/background.png differ
diff --git a/public/index.php b/public/index.php
new file mode 100644
index 0000000..ca75a68
--- /dev/null
+++ b/public/index.php
@@ -0,0 +1,18 @@
+access == 'user') {
+ Http::redirect('user.php');
+ } else if ($data->access == 'admin') {
+ Http::redirect('admin.php');
+ }
+}
+
+include 'template/header.html';
+include(APP_DIR . 'forms/login.php');
+include 'template/footer.html';
+?>
diff --git a/public/js/app.js b/public/js/app.js
new file mode 100644
index 0000000..50f51c0
--- /dev/null
+++ b/public/js/app.js
@@ -0,0 +1,27 @@
+
+/* Scale login form on page load */
+$(() => $('#login-form').addClass('scale-in'));
+
+/* Scale user tiles on page load */
+$(() => $('.user-tile').addClass('scale-in'));
+
+$(document).ready(function(){
+ // the "href" attribute of .modal-trigger must specify the modal ID that wants to be triggered
+ $('.modal').modal();
+});
+
+$('.dropdown-button').dropdown({
+ inDuration: 300,
+ outDuration: 225,
+ constrainWidth: false, // Does not change width of dropdown to that of the activator
+ hover: true, // Activate on hover
+ gutter: 0, // Spacing from edge
+ belowOrigin: true, // Displays dropdown below the button
+ alignment: 'right', // Displays dropdown with edge aligned to the left of button
+ stopPropagation: false // Stops event propagation
+ }
+);
+
+$(document).ready(function(){
+ $('.collapsible').collapsible();
+ });
diff --git a/public/login.php b/public/login.php
new file mode 100644
index 0000000..d7e39f7
--- /dev/null
+++ b/public/login.php
@@ -0,0 +1,25 @@
+login_attempts;
+
+if ($attempts >= 8) {
+ Http::redirect('index.php', ['error' => '2']);
+}
+
+$user = UserController::authenticate(Http::post_params());
+
+if (!$user) {
+ Http::redirect('index.php', ['error' => '1']);
+}
+
+setcookie(Config::cookie_name, $user->token());
+if ($user->access == 'user') {
+ Http::redirect('user.php');
+} else if ($user->access == 'admin') {
+ Http::redirect('admin.php');
+}
+
+?>
diff --git a/public/logout.php b/public/logout.php
new file mode 100644
index 0000000..35320c8
--- /dev/null
+++ b/public/logout.php
@@ -0,0 +1,8 @@
+
diff --git a/public/template/footer.html b/public/template/footer.html
new file mode 100644
index 0000000..6924fd7
--- /dev/null
+++ b/public/template/footer.html
@@ -0,0 +1,9 @@
+
+
+
+
+