Merge ../../BizApps/BusinessAppTemplate

# Conflicts:
#	LICENSE.md
#	README.md
#	pages/form.php
#	settings.template.php
master
Skylar Ittner 5 years ago
commit d1acda3349

@ -1,19 +1,7 @@
Copyright (c) 2017-2019 Netsyms Technologies. Copyright (c) 2017-2019 Netsyms Technologies. Some rights reserved.
If you modify and redistribute this project, you must replace the branding Licensed under the Mozilla Public License Version 2.0. Files without MPL header
assets with your own. comments, including third party code, may be under a different license.
The branding assets include:
* the application icon
* the Netsyms N punchcard logo
* the Netsyms for Business graph logo
If you are unsure if your usage is allowed, please contact us:
https://netsyms.com/contact
legal@netsyms.com
All other portions of this application,
unless otherwise noted (in comments, headers, etc), are licensed as follows:
Mozilla Public License Version 2.0 Mozilla Public License Version 2.0
================================== ==================================

@ -4,4 +4,4 @@ Camp Portal
An online registration system for the Prickly Pear District Cub Scout Day Camp. An online registration system for the Prickly Pear District Cub Scout Day Camp.
This project is almost definitely not useful to anyone as-is; it was built to This project is almost definitely not useful to anyone as-is; it was built to
client specifications. client specifications.

@ -21,11 +21,11 @@ if ($VARS['action'] !== "signout") {
*/ */
function returnToSender($msg, $arg = "") { function returnToSender($msg, $arg = "") {
global $VARS; global $VARS;
if ($arg == "") { $header = "Location: app.php?page=" . urlencode($VARS['source']) . "&msg=$msg";
header("Location: app.php?page=" . urlencode($VARS['source']) . "&msg=" . $msg); if ($arg != "") {
} else { $header .= "&arg=$arg";
header("Location: app.php?page=" . urlencode($VARS['source']) . "&msg=$msg&arg=$arg");
} }
header($header);
die(); die();
} }

@ -48,24 +48,48 @@ function getCensoredKey() {
/** /**
* Check if the request is allowed * Check if the request is allowed
* @global type $VARS * @global array $VARS
* @global type $database
* @return bool true if the request should continue, false if the request is bad * @return bool true if the request should continue, false if the request is bad
*/ */
function authenticate(): bool { function authenticate(): bool {
global $VARS, $database; global $VARS, $SETTINGS;
if (empty($VARS['key'])) { // HTTP basic auth
return false; if (!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW'])) {
$username = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];
} else if (!empty($VARS['username']) && !empty($VARS['password'])) {
$username = $VARS['username'];
$password = $VARS['password'];
} else { } else {
$key = $VARS['key']; return false;
if ($database->has('apikeys', ['key' => $key]) !== TRUE) { }
engageRateLimit(); $user = User::byUsername($username);
http_response_code(403); if (!$user->exists()) {
Log::insert(LogType::API_BAD_KEY, null, "Key: " . $key); return false;
return false; }
if ($user->checkPassword($password, true)) {
// Check that the user has permission to access the app
$perms = is_array($SETTINGS['api_permissions']) ? $SETTINGS['api_permissions'] : $SETTINGS['permissions'];
foreach ($perms as $perm) {
if (!$user->hasPermission($perm)) {
return false;
}
} }
return true;
}
return false;
}
/**
* Get the User whose credentials were used to make the request.
*/
function getRequestUser(): User {
global $VARS;
if (!empty($_SERVER['PHP_AUTH_USER'])) {
return User::byUsername($_SERVER['PHP_AUTH_USER']);
} else {
return User::byUsername($VARS['username']);
} }
return true;
} }
function checkVars($vars, $or = false) { function checkVars($vars, $or = false) {
@ -90,11 +114,13 @@ function checkVars($vars, $or = false) {
continue; continue;
} }
} }
$checkmethod = "is_$val";
if ($checkmethod($VARS[$key]) !== true) { if (strpos($val, "/") === 0) {
$ok[$key] = false; // regex
$ok[$key] = preg_match($val, $VARS[$key]) === 1;
} else { } else {
$ok[$key] = true; $checkmethod = "is_$val";
$ok[$key] = !($checkmethod($VARS[$key]) !== true);
} }
} }
if ($or) { if ($or) {

@ -10,6 +10,8 @@ require __DIR__ . '/../required.php';
require __DIR__ . '/functions.php'; require __DIR__ . '/functions.php';
require __DIR__ . '/apisettings.php'; require __DIR__ . '/apisettings.php';
header("Access-Control-Allow-Origin: *");
$VARS = $_GET; $VARS = $_GET;
if ($_SERVER['REQUEST_METHOD'] != "GET") { if ($_SERVER['REQUEST_METHOD'] != "GET") {
$VARS = array_merge($VARS, $_POST); $VARS = array_merge($VARS, $_POST);
@ -25,13 +27,14 @@ if (json_last_error() == JSON_ERROR_NONE) {
if (strpos($_SERVER['REQUEST_URI'], "/api.php") === FALSE) { if (strpos($_SERVER['REQUEST_URI'], "/api.php") === FALSE) {
$route = explode("/", substr($_SERVER['REQUEST_URI'], strpos($_SERVER['REQUEST_URI'], "api/") + 4)); $route = explode("/", substr($_SERVER['REQUEST_URI'], strpos($_SERVER['REQUEST_URI'], "api/") + 4));
if (count($route) > 1) { if (count($route) >= 1) {
$VARS["action"] = $route[0]; $VARS["action"] = $route[0];
} }
if (count($route) >= 2 && strpos($route[1], "?") !== 0) { if (count($route) >= 2 && strpos($route[1], "?") !== 0) {
$VARS["key"] = $route[1]; for ($i = 1; $i < count($route); $i++) {
if (empty($route[$i]) || strpos($route[$i], "=") === false) {
for ($i = 2; $i < count($route); $i++) { continue;
}
$key = explode("=", $route[$i], 2)[0]; $key = explode("=", $route[$i], 2)[0];
$val = explode("=", $route[$i], 2)[1]; $val = explode("=", $route[$i], 2)[1];
$VARS[$key] = $val; $VARS[$key] = $val;
@ -49,8 +52,9 @@ if (strpos($_SERVER['REQUEST_URI'], "/api.php") === FALSE) {
} }
if (!authenticate()) { if (!authenticate()) {
http_response_code(403); header('WWW-Authenticate: Basic realm="' . $SETTINGS['site_title'] . '"');
die("403 Unauthorized"); header('HTTP/1.1 401 Unauthorized');
die("401 Unauthorized: you need to supply valid credentials.");
} }
if (empty($VARS['action'])) { if (empty($VARS['action'])) {

@ -13,8 +13,19 @@ if (!empty($_SESSION['loggedin']) && $_SESSION['loggedin'] === true && !isset($_
die(); die();
} }
if (!empty($_GET['logout'])) { /**
// Show a logout message instead of immediately redirecting to login flow * Show a simple HTML page with a line of text and a button. Matches the UI of
* the AccountHub login flow.
*
* @global type $SETTINGS
* @global type $SECURE_NONCE
* @global type $Strings
* @param string $title Text to show, passed through i18n
* @param string $button Button text, passed through i18n
* @param string $url URL for the button
*/
function showHTML(string $title, string $button, string $url) {
global $SETTINGS, $SECURE_NONCE, $Strings;
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<meta charset="UTF-8"> <meta charset="UTF-8">
@ -26,7 +37,6 @@ if (!empty($_GET['logout'])) {
<link rel="icon" href="static/img/logo.svg"> <link rel="icon" href="static/img/logo.svg">
<link href="static/css/bootstrap.min.css" rel="stylesheet"> <link href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/svg-with-js.min.css" rel="stylesheet">
<style nonce="<?php echo $SECURE_NONCE; ?>"> <style nonce="<?php echo $SECURE_NONCE; ?>">
.display-5 { .display-5 {
font-size: 2.5rem; font-size: 2.5rem;
@ -40,11 +50,6 @@ if (!empty($_GET['logout'])) {
border: 1px solid grey; border: 1px solid grey;
border-radius: 15%; border-radius: 15%;
} }
.blank-image {
height: 100px;
margin: 2em auto;
}
</style> </style>
<div class="container mt-4"> <div class="container mt-4">
@ -54,24 +59,25 @@ if (!empty($_GET['logout'])) {
</div> </div>
<div class="col-12 text-center"> <div class="col-12 text-center">
<h1 class="display-5 mb-4"><?php $Strings->get("You have been logged out.") ?></h1> <h1 class="display-5 mb-4"><?php $Strings->get($title); ?></h1>
</div> </div>
<div class="col-12 col-sm-8 col-lg-6"> <div class="col-12 col-sm-8 col-lg-6">
<div class="card mt-4"> <div class="card mt-4">
<div class="card-body"> <div class="card-body">
<a href="./index.php" class="btn btn-primary btn-block"><?php $Strings->get("Log in again"); ?></a> <a href="<?php echo $url; ?>" class="btn btn-primary btn-block"><?php $Strings->get($button); ?></a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script src="static/js/fontawesome-all.min.js"></script>
<?php <?php
die();
} }
if (!empty($_GET['logout'])) {
showHTML("You have been logged out.", "Log in again", "./index.php");
die();
}
if (empty($_SESSION["login_code"])) { if (empty($_SESSION["login_code"])) {
$redirecttologin = true; $redirecttologin = true;
} else { } else {
@ -82,10 +88,17 @@ if (empty($_SESSION["login_code"])) {
} }
if (is_numeric($uidinfo['uid'])) { if (is_numeric($uidinfo['uid'])) {
$user = new User($uidinfo['uid'] * 1); $user = new User($uidinfo['uid'] * 1);
foreach ($SETTINGS['permissions'] as $perm) {
if (!$user->hasPermission($perm)) {
showHTML("no access permission", "sign out", "./action.php?action=signout");
die();
}
}
Session::start($user); Session::start($user);
$_SESSION["login_code"] = null; $_SESSION["login_code"] = null;
header('Location: app.php'); header('Location: app.php');
die("Logged in, go to app.php"); showHTML("Logged in", "Continue", "./app.php");
die();
} else { } else {
throw new Exception(); throw new Exception();
} }
@ -108,7 +121,10 @@ if ($redirecttologin) {
$_SESSION["login_code"] = $codedata["code"]; $_SESSION["login_code"] = $codedata["code"];
header("Location: " . $codedata["loginurl"] . "?code=" . htmlentities($codedata["code"]) . "&redirect=" . htmlentities($redirecturl)); $locationurl = $codedata["loginurl"] . "?code=" . htmlentities($codedata["code"]) . "&redirect=" . htmlentities($redirecturl);
header("Location: $locationurl");
showHTML("Continue", "Continue", $locationurl);
die();
} catch (Exception $ex) { } catch (Exception $ex) {
sendError($ex->getMessage()); sendError($ex->getMessage());
} }

@ -1,16 +1,7 @@
{ {
"You have been logged out.": "You have been logged out.",
"Log in again": "Log in again",
"login server unavailable": "Login server unavailable. Try again later or contact technical support.",
"welcome user": "Welcome, {user}!",
"sign out": "Sign out", "sign out": "Sign out",
"settings": "Settings",
"options": "Options",
"404 error": "404 Error", "404 error": "404 Error",
"page not found": "Page not found.", "page not found": "Page not found.",
"invalid parameters": "Invalid request parameters.", "invalid parameters": "Invalid request parameters.",
"login server error": "The login server returned an error: {arg}", "login server error": "The login server returned an error: {arg}"
"login server user data error": "The login server refused to provide account information. Try again or contact technical support.",
"captcha error": "There was a problem with the CAPTCHA (robot test). Try again.",
"no access permission": "You do not have permission to access this system."
} }

@ -0,0 +1,8 @@
{
"You have been logged out.": "You have been logged out.",
"Log in again": "Log in again",
"login server unavailable": "Login server unavailable. Try again later or contact technical support.",
"no access permission": "You do not have permission to access this system.",
"Logged in": "Logged in",
"Continue": "Continue"
}

@ -116,6 +116,41 @@ class FormBuilder {
$this->items[] = $item; $this->items[] = $item;
} }
/**
* Add a text input.
*
* @param string $name Element name
* @param string $value Element value
* @param bool $required If the element is required for form submission.
* @param string $id Element ID
* @param string $label Text label to display near the input
* @param string $icon FontAwesome icon (example: "fas fa-toilet-paper")
* @param int $width Bootstrap column width for the input, out of 12.
* @param int $minlength Minimum number of characters for the input.
* @param int $maxlength Maximum number of characters for the input.
* @param string $pattern Regex pattern for custom client-side validation.
* @param string $error Message to show if the input doesn't validate.
*/
public function addTextInput(string $name, string $value = "", bool $required = true, string $id = "", string $label = "", string $icon = "", int $width = 4, int $minlength = 1, int $maxlength = 100, string $pattern = "", string $error = "") {
$this->addInput($name, $value, "text", $required, $id, null, $label, $icon, $width, $minlength, $maxlength, $pattern, $error);
}
/**
* Add a select dropdown.
*
* @param string $name Element name
* @param string $value Element value
* @param bool $required If the element is required for form submission.
* @param string $id Element ID
* @param array $options Array of [value => text] pairs for a select element
* @param string $label Text label to display near the input
* @param string $icon FontAwesome icon (example: "fas fa-toilet-paper")
* @param int $width Bootstrap column width for the input, out of 12.
*/
public function addSelect(string $name, string $value = "", bool $required = true, string $id = null, array $options = null, string $label = "", string $icon = "", int $width = 4) {
$this->addInput($name, $value, "select", $required, $id, $options, $label, $icon, $width);
}
/** /**
* Add a button to the form. * Add a button to the form.
* *
@ -173,33 +208,65 @@ HTMLTOP;
$required = $item["required"] ? "required" : ""; $required = $item["required"] ? "required" : "";
$id = empty($item["id"]) ? "" : "id=\"$item[id]\""; $id = empty($item["id"]) ? "" : "id=\"$item[id]\"";
$pattern = empty($item["pattern"]) ? "" : "pattern=\"$item[pattern]\""; $pattern = empty($item["pattern"]) ? "" : "pattern=\"$item[pattern]\"";
if (empty($item['type'])) {
$item['type'] = "text";
}
$itemhtml = ""; $itemhtml = "";
$itemlabel = "";
if ($item['type'] == "textarea") {
$itemlabel = "<label class=\"mb-0\"><i class=\"$item[icon]\"></i> $item[label]:</label>";
} else if ($item['type'] != "checkbox") {
$itemlabel = "<label class=\"mb-0\">$item[label]:</label>";
}
$strippedlabel = strip_tags($item['label']);
$itemhtml .= <<<ITEMTOP $itemhtml .= <<<ITEMTOP
\n\n <div class="col-12 col-md-$item[width]"> \n\n <div class="col-12 col-md-$item[width]">
<div class="form-group mb-3"> <div class="form-group mb-3">
<label class="mb-0">$item[label]:</label> $itemlabel
<div class="input-group"> ITEMTOP;
$inputgrouptop = <<<INPUTG
\n <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="$item[icon]"></i></span> <span class="input-group-text"><i class="$item[icon]"></i></span>
</div> </div>
ITEMTOP; INPUTG;
if (empty($item['type']) || $item['type'] != "select") { switch ($item['type']) {
$itemhtml .= <<<INPUT case "select":
\n <input type="$item[type]" name="$item[name]" $id class="form-control" aria-label="$item[label]" minlength="$item[minlength]" maxlength="$item[maxlength]" $pattern value="$item[value]" $required /> $itemhtml .= $inputgrouptop;
INPUT; $itemhtml .= <<<SELECT
} else { \n <select class="form-control" name="$item[name]" aria-label="$strippedlabel" $required>
$itemhtml .= <<<SELECT
\n <select class="form-control" name="$item[name]" aria-label="$item[label]" $required>
SELECT; SELECT;
foreach ($item['options'] as $value => $label) { foreach ($item['options'] as $value => $label) {
$selected = ""; $selected = "";
if (!empty($item['value']) && $value == $item['value']) { if (!empty($item['value']) && $value == $item['value']) {
$selected = " selected"; $selected = " selected";
}
$itemhtml .= "\n <option value=\"$value\"$selected>$label</option>";
} }
$itemhtml .= "\n <option value=\"$value\"$selected>$label</option>"; $itemhtml .= "\n </select>";
} break;
$itemhtml .= "\n </select>"; case "checkbox":
$itemhtml .= $inputgrouptop;
$itemhtml .= <<<CHECKBOX
\n <div class="form-group form-check">
<input type="checkbox" name="$item[name]" $id class="form-check-input" value="$item[value]" $required aria-label="$strippedlabel">
<label class="form-check-label">$item[label]</label>
</div>
CHECKBOX;
break;
case "textarea":
$val = htmlentities($item['value']);
$itemhtml .= <<<TEXTAREA
\n <textarea class="form-control" id="info" name="$item[name]" aria-label="$strippedlabel" minlength="$item[minlength]" maxlength="$item[maxlength]" $required>$val</textarea>
TEXTAREA;
break;
default:
$itemhtml .= $inputgrouptop;
$itemhtml .= <<<INPUT
\n <input type="$item[type]" name="$item[name]" $id class="form-control" aria-label="$strippedlabel" minlength="$item[minlength]" maxlength="$item[maxlength]" $pattern value="$item[value]" $required />
INPUT;
break;
} }
if (!empty($item["error"])) { if (!empty($item["error"])) {
@ -209,9 +276,11 @@ SELECT;
</div> </div>
ERROR; ERROR;
} }
if ($item["type"] != "textarea") {
$itemhtml .= "\n </div>";
}
$itemhtml .= <<<ITEMBOTTOM $itemhtml .= <<<ITEMBOTTOM
\n </div> \n </div>
</div>
</div>\n </div>\n
ITEMBOTTOM; ITEMBOTTOM;
$html .= $itemhtml; $html .= $itemhtml;
@ -224,7 +293,7 @@ ITEMBOTTOM;
HTMLBOTTOM; HTMLBOTTOM;
if (!empty($this->buttons)) { if (!empty($this->buttons)) {
$html .= "\n <div class=\"card-footer\">"; $html .= "\n <div class=\"card-footer d-flex\">";
foreach ($this->buttons as $btn) { foreach ($this->buttons as $btn) {
$btnhtml = ""; $btnhtml = "";
$inner = "<i class=\"$btn[icon]\"></i> $btn[text]"; $inner = "<i class=\"$btn[icon]\"></i> $btn[text]";

@ -45,29 +45,6 @@ class Login {
return Login::LOGIN_OK; return Login::LOGIN_OK;
} }
public static function verifyCaptcha(string $session, string $answer, string $url): bool {
$data = [
'session_id' => $session,
'answer_id' => $answer,
'action' => "verify"
];
$options = [
'http' => [
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($data)
]
];
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
$resp = json_decode($result, TRUE);
if (!$resp['result']) {
return false;
} else {
return true;
}
}
/** /**
* Check the login server API for sanity * Check the login server API for sanity
* @return boolean true if OK, else false * @return boolean true if OK, else false

@ -88,10 +88,11 @@ class User {
/** /**
* Check the given plaintext password against the stored hash. * Check the given plaintext password against the stored hash.
* @param string $password * @param string $password
* @param bool $apppass Set to true to enforce app passwords when 2fa is on.
* @return bool * @return bool
*/ */
function checkPassword(string $password): bool { function checkPassword(string $password, bool $apppass = false): bool {
$resp = AccountHubApi::get("auth", ['username' => $this->username, 'password' => $password]); $resp = AccountHubApi::get("auth", ['username' => $this->username, 'password' => $password, 'apppass' => ($apppass ? "1" : "0")]);
if ($resp['status'] == "OK") { if ($resp['status'] == "OK") {
return true; return true;
} else { } else {
@ -99,6 +100,7 @@ class User {
} }
} }
function check2fa(string $code): bool { function check2fa(string $code): bool {
if (!$this->has2fa) { if (!$this->has2fa) {
return true; return true;

@ -8,10 +8,6 @@
* Mobile app API * Mobile app API
*/ */
// The name of the permission needed to log in.
// Set to null if you don't need it.
$access_permission = null;
require __DIR__ . "/../required.php"; require __DIR__ . "/../required.php";
header('Content-Type: application/json'); header('Content-Type: application/json');
@ -70,13 +66,14 @@ switch ($VARS['action']) {
if ($user->exists()) { if ($user->exists()) {
if ($user->getStatus()->getString() == "NORMAL") { if ($user->getStatus()->getString() == "NORMAL") {
if ($user->checkPassword($VARS['password'])) { if ($user->checkPassword($VARS['password'])) {
if (is_null($access_permission) || $user->hasPermission($access_permission)) { foreach ($SETTINGS['permissions'] as $perm) {
Session::start($user); if (!$user->hasPermission($perm)) {
$_SESSION['mobile'] = true; exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("no permission", false)]));
exit(json_encode(["status" => "OK"])); }
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("no admin permission", false)]));
} }
Session::start($user);
$_SESSION['mobile'] = true;
exit(json_encode(["status" => "OK"]));
} }
} }
} }

@ -32,7 +32,6 @@ session_start(); // stick some cookies in it
// renew session cookie // renew session cookie
setcookie(session_name(), session_id(), time() + $session_length, "/", false, false); setcookie(session_name(), session_id(), time() + $session_length, "/", false, false);
$captcha_server = ($SETTINGS['captcha']['enabled'] === true ? preg_replace("/http(s)?:\/\//", "", $SETTINGS['captcha']['server']) : "");
if ($_SESSION['mobile'] === TRUE) { if ($_SESSION['mobile'] === TRUE) {
header("Content-Security-Policy: " header("Content-Security-Policy: "
. "default-src 'self';" . "default-src 'self';"
@ -42,8 +41,8 @@ if ($_SESSION['mobile'] === TRUE) {
. "frame-src 'none'; " . "frame-src 'none'; "
. "font-src 'self'; " . "font-src 'self'; "
. "connect-src *; " . "connect-src *; "
. "style-src 'self' 'unsafe-inline' $captcha_server; " . "style-src 'self' 'unsafe-inline'; "
. "script-src 'self' 'unsafe-inline' $captcha_server"); . "script-src 'self' 'unsafe-inline'");
} else { } else {
header("Content-Security-Policy: " header("Content-Security-Policy: "
. "default-src 'self';" . "default-src 'self';"
@ -53,8 +52,8 @@ if ($_SESSION['mobile'] === TRUE) {
. "frame-src 'none'; " . "frame-src 'none'; "
. "font-src 'self'; " . "font-src 'self'; "
. "connect-src *; " . "connect-src *; "
. "style-src 'self' 'nonce-$SECURE_NONCE' $captcha_server; " . "style-src 'self' 'nonce-$SECURE_NONCE'; "
. "script-src 'self' 'nonce-$SECURE_NONCE' $captcha_server"); . "script-src 'self' 'nonce-$SECURE_NONCE'");
} }
// //
@ -132,11 +131,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
define("GET", true); define("GET", true);
} }
function dieifnotloggedin() { function dieifnotloggedin() {
global $SETTINGS;
if ($_SESSION['loggedin'] != true) { if ($_SESSION['loggedin'] != true) {
sendError("Session expired. Please log out and log in again."); sendError("Session expired. Please log out and log in again.");
} }
$user = new User($_SESSION['uid']);
foreach ($SETTINGS['permissions'] as $perm) {
if (!$user->hasPermission($perm)) {
session_destroy();
die("You don't have permission to be here.");
}
}
} }
/** /**
@ -157,8 +163,17 @@ function checkDBError($specials = []) {
} }
function redirectIfNotLoggedIn() { function redirectIfNotLoggedIn() {
global $SETTINGS;
if ($_SESSION['loggedin'] !== TRUE) { if ($_SESSION['loggedin'] !== TRUE) {
header('Location: ' . $SETTINGS['url'] . '/index.php'); header('Location: ' . $SETTINGS['url'] . '/index.php');
die(); die();
} }
$user = new User($_SESSION['uid']);
foreach ($SETTINGS['permissions'] as $perm) {
if (!$user->hasPermission($perm)) {
session_destroy();
header('Location: ./index.php');
die("You don't have permission to be here.");
}
}
} }

@ -59,6 +59,15 @@ $SETTINGS = [
"enabled" => false, "enabled" => false,
"server" => "https://captcheck.netsyms.com" "server" => "https://captcheck.netsyms.com"
], ],
// List of required user permissions to access this app.
"permissions" => [
],
// List of permissions required for API access. Remove to use the value of
// "permissions" instead.
"api_permissions" => [
],
// For supported values, see http://php.net/manual/en/timezones.php
"timezone" => "America/Denver",
// Language to use for localization. See langs folder to add a language. // Language to use for localization. See langs folder to add a language.
"language" => "en", "language" => "en",
// Shown in the footer of all the pages. // Shown in the footer of all the pages.

File diff suppressed because one or more lines are too long

@ -1,15 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
.banner-image {
max-height: 100px;
margin: 2em auto;
border: 1px solid grey;
border-radius: 15%;
}
.footer {
margin-top: 10em;
text-align: center;
}

@ -1,5 +1 @@
/*! .svg-inline--fa,svg:not(:root).svg-inline--fa{overflow:visible}.svg-inline--fa{display:inline-block;font-size:inherit;height:1em;vertical-align:-.125em}.svg-inline--fa.fa-lg{vertical-align:-.225em}.svg-inline--fa.fa-w-1{width:.0625em}.svg-inline--fa.fa-w-2{width:.125em}.svg-inline--fa.fa-w-3{width:.1875em}.svg-inline--fa.fa-w-4{width:.25em}.svg-inline--fa.fa-w-5{width:.3125em}.svg-inline--fa.fa-w-6{width:.375em}.svg-inline--fa.fa-w-7{width:.4375em}.svg-inline--fa.fa-w-8{width:.5em}.svg-inline--fa.fa-w-9{width:.5625em}.svg-inline--fa.fa-w-10{width:.625em}.svg-inline--fa.fa-w-11{width:.6875em}.svg-inline--fa.fa-w-12{width:.75em}.svg-inline--fa.fa-w-13{width:.8125em}.svg-inline--fa.fa-w-14{width:.875em}.svg-inline--fa.fa-w-15{width:.9375em}.svg-inline--fa.fa-w-16{width:1em}.svg-inline--fa.fa-w-17{width:1.0625em}.svg-inline--fa.fa-w-18{width:1.125em}.svg-inline--fa.fa-w-19{width:1.1875em}.svg-inline--fa.fa-w-20{width:1.25em}.svg-inline--fa.fa-pull-left{margin-right:.3em;width:auto}.svg-inline--fa.fa-pull-right{margin-left:.3em;width:auto}.svg-inline--fa.fa-border{height:1.5em}.svg-inline--fa.fa-li{width:2em}.svg-inline--fa.fa-fw{width:1.25em}.fa-layers svg.svg-inline--fa{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.fa-layers{display:inline-block;height:1em;position:relative;text-align:center;vertical-align:-.125em;width:1em}.fa-layers svg.svg-inline--fa{transform-origin:center center}.fa-layers-counter,.fa-layers-text{display:inline-block;position:absolute;text-align:center}.fa-layers-text{left:50%;top:50%;transform:translate(-50%,-50%);transform-origin:center center}.fa-layers-counter{background-color:#ff253a;border-radius:1em;box-sizing:border-box;color:#fff;height:1.5em;line-height:1;max-width:5em;min-width:1.5em;overflow:hidden;padding:.25em;right:0;text-overflow:ellipsis;top:0;transform:scale(.25);transform-origin:top right}.fa-layers-bottom-right{bottom:0;right:0;top:auto;transform:scale(.25);transform-origin:bottom right}.fa-layers-bottom-left{bottom:0;left:0;right:auto;top:auto;transform:scale(.25);transform-origin:bottom left}.fa-layers-top-right{right:0;top:0;transform:scale(.25);transform-origin:top right}.fa-layers-top-left{left:0;right:auto;top:0;transform:scale(.25);transform-origin:top left}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{animation:fa-spin 2s infinite linear}.fa-pulse{animation:fa-spin 1s infinite steps(8)}@keyframes fa-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";transform:scaleX(-1)}.fa-flip-vertical{transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{display:inline-block;height:2em;position:relative;width:2.5em}.fa-stack-1x,.fa-stack-2x{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.svg-inline--fa.fa-stack-1x{height:1em;width:1.25em}.svg-inline--fa.fa-stack-2x{height:2em;width:2.5em}.fa-inverse{color:#fff}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}
* Font Awesome Free 5.6.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
*/
.svg-inline--fa,svg:not(:root).svg-inline--fa{overflow:visible}.svg-inline--fa{display:inline-block;font-size:inherit;height:1em;vertical-align:-.125em}.svg-inline--fa.fa-lg{vertical-align:-.225em}.svg-inline--fa.fa-w-1{width:.0625em}.svg-inline--fa.fa-w-2{width:.125em}.svg-inline--fa.fa-w-3{width:.1875em}.svg-inline--fa.fa-w-4{width:.25em}.svg-inline--fa.fa-w-5{width:.3125em}.svg-inline--fa.fa-w-6{width:.375em}.svg-inline--fa.fa-w-7{width:.4375em}.svg-inline--fa.fa-w-8{width:.5em}.svg-inline--fa.fa-w-9{width:.5625em}.svg-inline--fa.fa-w-10{width:.625em}.svg-inline--fa.fa-w-11{width:.6875em}.svg-inline--fa.fa-w-12{width:.75em}.svg-inline--fa.fa-w-13{width:.8125em}.svg-inline--fa.fa-w-14{width:.875em}.svg-inline--fa.fa-w-15{width:.9375em}.svg-inline--fa.fa-w-16{width:1em}.svg-inline--fa.fa-w-17{width:1.0625em}.svg-inline--fa.fa-w-18{width:1.125em}.svg-inline--fa.fa-w-19{width:1.1875em}.svg-inline--fa.fa-w-20{width:1.25em}.svg-inline--fa.fa-pull-left{margin-right:.3em;width:auto}.svg-inline--fa.fa-pull-right{margin-left:.3em;width:auto}.svg-inline--fa.fa-border{height:1.5em}.svg-inline--fa.fa-li{width:2em}.svg-inline--fa.fa-fw{width:1.25em}.fa-layers svg.svg-inline--fa{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.fa-layers{display:inline-block;height:1em;position:relative;text-align:center;vertical-align:-.125em;width:1em}.fa-layers svg.svg-inline--fa{transform-origin:center center}.fa-layers-counter,.fa-layers-text{display:inline-block;position:absolute;text-align:center}.fa-layers-text{left:50%;top:50%;transform:translate(-50%,-50%);transform-origin:center center}.fa-layers-counter{background-color:#ff253a;border-radius:1em;box-sizing:border-box;color:#fff;height:1.5em;line-height:1;max-width:5em;min-width:1.5em;overflow:hidden;padding:.25em;right:0;text-overflow:ellipsis;top:0;transform:scale(.25);transform-origin:top right}.fa-layers-bottom-right{bottom:0;right:0;top:auto;transform:scale(.25);transform-origin:bottom right}.fa-layers-bottom-left{bottom:0;left:0;right:auto;top:auto;transform:scale(.25);transform-origin:bottom left}.fa-layers-top-right{right:0;top:0;transform:scale(.25);transform-origin:top right}.fa-layers-top-left{left:0;right:auto;top:0;transform:scale(.25);transform-origin:top left}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{animation:fa-spin 2s infinite linear}.fa-pulse{animation:fa-spin 1s infinite steps(8)}@keyframes fa-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";transform:scaleX(-1)}.fa-flip-vertical{transform:scaleY(-1)}.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-horizontal.fa-flip-vertical{transform:scale(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{display:inline-block;height:2em;position:relative;width:2.5em}.fa-stack-1x,.fa-stack-2x{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.svg-inline--fa.fa-stack-1x{height:1em;width:1.25em}.svg-inline--fa.fa-stack-2x{height:2em;width:2.5em}.fa-inverse{color:#fff}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}

@ -13,7 +13,7 @@ $(document).ready(function () {
var gone = 20; var gone = 20;
var msgticker = setInterval(function () { var msgticker = setInterval(function () {
if ($('#msg-alert-box .alert:hover').length) { if ($("#msg-alert-box .alert:hover").length) {
msginteractiontick = 0; msginteractiontick = 0;
} else { } else {
msginteractiontick++; msginteractiontick++;
@ -55,7 +55,6 @@ $(document).ready(function () {
$("#msg-alert-box").on("mouseenter", function () { $("#msg-alert-box").on("mouseenter", function () {
$("#msg-alert-box").css("opacity", "1"); $("#msg-alert-box").css("opacity", "1");
msginteractiontick = 0; msginteractiontick = 0;
console.log("👈😎👈 zoop");
}); });
$("#msg-alert-box").on("click", ".close", function (e) { $("#msg-alert-box").on("click", ".close", function (e) {
$("#msg-alert-box").fadeOut("slow"); $("#msg-alert-box").fadeOut("slow");

File diff suppressed because one or more lines are too long

@ -12,5 +12,5 @@ $("#savebutton").click(function (event) {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
} }
form.addClass('was-validated'); form.addClass("was-validated");
}); });

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save