Browse Source

Merge BusinessAppTemplate

master
Skylar Ittner 1 month ago
parent
commit
a5e20f87fd

+ 3
- 15
LICENSE.md View File

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

+ 1
- 1
README.md View File

@@ -2,4 +2,4 @@ NickelBox
2 2
 =========
3 3
 
4 4
 NickelBox is a point of sale app.  It integrates with BinStack for inventory
5
-management.
5
+management.

+ 13
- 13
action.php View File

@@ -21,11 +21,11 @@ if ($VARS['action'] !== "signout") {
21 21
  */
22 22
 function returnToSender($msg, $arg = "") {
23 23
     global $VARS;
24
-    if ($arg == "") {
25
-        header("Location: app.php?page=" . urlencode($VARS['source']) . "&msg=" . $msg);
26
-    } else {
27
-        header("Location: app.php?page=" . urlencode($VARS['source']) . "&msg=$msg&arg=$arg");
24
+    $header = "Location: app.php?page=" . urlencode($VARS['source']) . "&msg=$msg";
25
+    if ($arg != "") {
26
+        $header .= "&arg=$arg";
28 27
     }
28
+    header($header);
29 29
     die();
30 30
 }
31 31
 
@@ -35,7 +35,7 @@ switch ($VARS['action']) {
35 35
         $error = null;
36 36
         $oktx = null;
37 37
         $database->action(function ($database) {
38
-            global $VARS, $binstack, $error, $oktx;
38
+            global $VARS, $binstack, $Strings, $error, $oktx;
39 39
 
40 40
             if (empty($VARS['items'])) {
41 41
                 $error = $Strings->get("no items", false);
@@ -215,7 +215,7 @@ switch ($VARS['action']) {
215 215
         $error = null;
216 216
         $oktx = null;
217 217
         $database->action(function ($database) {
218
-            global $VARS, $binstack, $error, $oktx;
218
+            global $VARS, $binstack, $Strings, $error, $oktx;
219 219
 
220 220
             $items = $VARS['items'];
221 221
             $payments = $VARS['payments'];
@@ -438,7 +438,7 @@ switch ($VARS['action']) {
438 438
         exit(json_encode(["status" => "OK", "transactions" => $transactions]));
439 439
     case "itemsearch":
440 440
         header("Content-Type: application/json");
441
-        if (!is_empty($VARS['q'])) {
441
+        if (!empty($VARS['q'])) {
442 442
             $where["AND"]["OR"] = [
443 443
                 "name[~]" => $VARS['q'],
444 444
                 "code1[~]" => $VARS['q'],
@@ -499,7 +499,7 @@ switch ($VARS['action']) {
499 499
         exit(json_encode(["status" => "OK", "items" => $items]));
500 500
     case "customersearch":
501 501
         header("Content-Type: application/json");
502
-        if (!is_empty($VARS['q'])) {
502
+        if (!empty($VARS['q'])) {
503 503
             $where["AND"]["OR"] = [
504 504
                 "customerid" => $VARS['q'],
505 505
                 "name[~]" => $VARS['q'],
@@ -533,7 +533,7 @@ switch ($VARS['action']) {
533 533
         break;
534 534
     case "editcustomer":
535 535
         $insert = true;
536
-        if (is_empty($VARS['id'])) {
536
+        if (empty($VARS['id'])) {
537 537
             $insert = true;
538 538
         } else {
539 539
             if ($database->has('customers', ['customerid' => $VARS['id']])) {
@@ -542,7 +542,7 @@ switch ($VARS['action']) {
542 542
                 returnToSender("invalid_customerid");
543 543
             }
544 544
         }
545
-        if (is_empty($VARS['name'])) {
545
+        if (empty($VARS['name'])) {
546 546
             returnToSender('invalid_parameters');
547 547
         }
548 548
 
@@ -673,7 +673,7 @@ switch ($VARS['action']) {
673 673
                 returnToSender("invalid_parameters");
674 674
             }
675 675
         }
676
-        if (is_empty($VARS['name'])) {
676
+        if (empty($VARS['name'])) {
677 677
             returnToSender('invalid_parameters');
678 678
         }
679 679
 
@@ -742,7 +742,7 @@ switch ($VARS['action']) {
742 742
             }
743 743
         }
744 744
 
745
-        if ($insert && (is_empty($code) || $database->has('certificates', ['certcode' => $code]))) {
745
+        if ($insert && (empty($code) || $database->has('certificates', ['certcode' => $code]))) {
746 746
             do {
747 747
                 $code = random_int(100000000000, 999999999999);
748 748
             } while ($database->has('certificates', ['certcode' => $code]));
@@ -775,6 +775,6 @@ switch ($VARS['action']) {
775 775
         exit(json_encode(["status" => "OK"]));
776 776
     case "signout":
777 777
         session_destroy();
778
-        header('Location: index.php');
778
+        header('Location: index.php?logout=1');
779 779
         die("Logged out.");
780 780
 }

+ 2
- 31
api.php View File

@@ -4,35 +4,6 @@
4 4
  * License, v. 2.0. If a copy of the MPL was not distributed with this
5 5
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 6
 
7
-/**
8
- * Simple JSON API to allow other apps to access data from this app.
9
- *
10
- * Requests can be sent via either GET or POST requests.  POST is recommended
11
- * as it has a lower chance of being logged on the server, exposing unencrypted
12
- * user passwords.
13
- */
14
-require __DIR__ . '/required.php';
15
-header("Content-Type: application/json");
16 7
 
17
-$username = $VARS['username'];
18
-$password = $VARS['password'];
19
-$user = User::byUsername($username);
20
-if ($user->exists() !== true || Login::auth($username, $password) !== Login::LOGIN_OK) {
21
-    header("HTTP/1.1 403 Unauthorized");
22
-    die("\"403 Unauthorized\"");
23
-}
24
-
25
-// query max results
26
-$max = 20;
27
-if (preg_match("/^[0-9]+$/", $VARS['max']) === 1 && $VARS['max'] <= 1000) {
28
-    $max = (int) $VARS['max'];
29
-}
30
-
31
-switch ($VARS['action']) {
32
-    case "ping":
33
-        $out = ["status" => "OK", "maxresults" => $max, "pong" => true];
34
-        exit(json_encode($out));
35
-    default:
36
-        header("HTTP/1.1 400 Bad Request");
37
-        die("\"400 Bad Request\"");
38
-}
8
+// Load in new API from legacy location (a.k.a. here)
9
+require __DIR__ . "/api/index.php";

+ 5
- 0
api/.htaccess View File

@@ -0,0 +1,5 @@
1
+# Rewrite for Nextcloud Notes API
2
+<IfModule mod_rewrite.c>
3
+    RewriteEngine on
4
+    RewriteRule ([a-zA-Z0-9]+) index.php?action=$1 [PT]
5
+</IfModule>

+ 9
- 0
api/actions/ping.php View File

@@ -0,0 +1,9 @@
1
+<?php
2
+
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+sendJsonResp();

+ 15
- 0
api/apisettings.php View File

@@ -0,0 +1,15 @@
1
+<?php
2
+
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+$APIS = [
10
+    "ping" => [
11
+        "load" => "ping.php",
12
+        "vars" => [
13
+        ]
14
+    ]
15
+];

+ 149
- 0
api/functions.php View File

@@ -0,0 +1,149 @@
1
+<?php
2
+
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+/**
10
+ * Build and send a simple JSON response.
11
+ * @param string $msg A message
12
+ * @param string $status "OK" or "ERROR"
13
+ * @param array $data More JSON data
14
+ */
15
+function sendJsonResp(string $msg = null, string $status = "OK", array $data = null) {
16
+    $resp = [];
17
+    if (!is_null($data)) {
18
+        $resp = $data;
19
+    }
20
+    if (!is_null($msg)) {
21
+        $resp["msg"] = $msg;
22
+    }
23
+    $resp["status"] = $status;
24
+    header("Content-Type: application/json");
25
+    exit(json_encode($resp));
26
+}
27
+
28
+function exitWithJson(array $json) {
29
+    header("Content-Type: application/json");
30
+    exit(json_encode($json));
31
+}
32
+
33
+/**
34
+ * Get the API key with most of the characters replaced with *s.
35
+ * @global string $key
36
+ * @return string
37
+ */
38
+function getCensoredKey() {
39
+    global $key;
40
+    $resp = $key;
41
+    if (strlen($key) > 5) {
42
+        for ($i = 2; $i < strlen($key) - 2; $i++) {
43
+            $resp[$i] = "*";
44
+        }
45
+    }
46
+    return $resp;
47
+}
48
+
49
+/**
50
+ * Check if the request is allowed
51
+ * @global array $VARS
52
+ * @return bool true if the request should continue, false if the request is bad
53
+ */
54
+function authenticate(): bool {
55
+    global $VARS, $SETTINGS;
56
+    // HTTP basic auth
57
+    if (!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW'])) {
58
+        $username = $_SERVER['PHP_AUTH_USER'];
59
+        $password = $_SERVER['PHP_AUTH_PW'];
60
+    } else if (!empty($VARS['username']) && !empty($VARS['password'])) {
61
+        $username = $VARS['username'];
62
+        $password = $VARS['password'];
63
+    } else {
64
+        return false;
65
+    }
66
+    $user = User::byUsername($username);
67
+    if (!$user->exists()) {
68
+        return false;
69
+    }
70
+    if ($user->checkPassword($password, true)) {
71
+        // Check that the user has permission to access the app
72
+        $perms = is_array($SETTINGS['api_permissions']) ? $SETTINGS['api_permissions'] : $SETTINGS['permissions'];
73
+        foreach ($perms as $perm) {
74
+            if (!$user->hasPermission($perm)) {
75
+                return false;
76
+            }
77
+        }
78
+        return true;
79
+    }
80
+    return false;
81
+}
82
+
83
+/**
84
+ * Get the User whose credentials were used to make the request.
85
+ */
86
+function getRequestUser(): User {
87
+    global $VARS;
88
+    if (!empty($_SERVER['PHP_AUTH_USER'])) {
89
+        return User::byUsername($_SERVER['PHP_AUTH_USER']);
90
+    } else {
91
+        return User::byUsername($VARS['username']);
92
+    }
93
+}
94
+
95
+function checkVars($vars, $or = false) {
96
+    global $VARS;
97
+    $ok = [];
98
+    foreach ($vars as $key => $val) {
99
+        if (strpos($key, "OR") === 0) {
100
+            checkVars($vars[$key], true);
101
+            continue;
102
+        }
103
+
104
+        // Only check type of optional variables if they're set, and don't
105
+        // mark them as bad if they're not set
106
+        if (strpos($key, " (optional)") !== false) {
107
+            $key = str_replace(" (optional)", "", $key);
108
+            if (empty($VARS[$key])) {
109
+                continue;
110
+            }
111
+        } else {
112
+            if (empty($VARS[$key])) {
113
+                $ok[$key] = false;
114
+                continue;
115
+            }
116
+        }
117
+
118
+        if (strpos($val, "/") === 0) {
119
+            // regex
120
+            $ok[$key] = preg_match($val, $VARS[$key]) === 1;
121
+        } else {
122
+            $checkmethod = "is_$val";
123
+            $ok[$key] = !($checkmethod($VARS[$key]) !== true);
124
+        }
125
+    }
126
+    if ($or) {
127
+        $success = false;
128
+        $bad = "";
129
+        foreach ($ok as $k => $v) {
130
+            if ($v) {
131
+                $success = true;
132
+                break;
133
+            } else {
134
+                $bad = $k;
135
+            }
136
+        }
137
+        if (!$success) {
138
+            http_response_code(400);
139
+            die("400 Bad request: variable $bad is missing or invalid");
140
+        }
141
+    } else {
142
+        foreach ($ok as $key => $bool) {
143
+            if (!$bool) {
144
+                http_response_code(400);
145
+                die("400 Bad request: variable $key is missing or invalid");
146
+            }
147
+        }
148
+    }
149
+}

+ 81
- 0
api/index.php View File

@@ -0,0 +1,81 @@
1
+<?php
2
+
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+require __DIR__ . '/../required.php';
10
+require __DIR__ . '/functions.php';
11
+require __DIR__ . '/apisettings.php';
12
+
13
+header("Access-Control-Allow-Origin: *");
14
+
15
+$VARS = $_GET;
16
+if ($_SERVER['REQUEST_METHOD'] != "GET") {
17
+    $VARS = array_merge($VARS, $_POST);
18
+}
19
+
20
+$requestbody = file_get_contents('php://input');
21
+$requestjson = json_decode($requestbody, TRUE);
22
+if (json_last_error() == JSON_ERROR_NONE) {
23
+    $VARS = array_merge($VARS, $requestjson);
24
+}
25
+
26
+// If we're not using the old api.php file, allow more flexible requests
27
+if (strpos($_SERVER['REQUEST_URI'], "/api.php") === FALSE) {
28
+    $route = explode("/", substr($_SERVER['REQUEST_URI'], strpos($_SERVER['REQUEST_URI'], "api/") + 4));
29
+
30
+    if (count($route) >= 1) {
31
+        $VARS["action"] = $route[0];
32
+    }
33
+    if (count($route) >= 2 && strpos($route[1], "?") !== 0) {
34
+        for ($i = 1; $i < count($route); $i++) {
35
+            if (empty($route[$i]) || strpos($route[$i], "=") === false) {
36
+                continue;
37
+            }
38
+            $key = explode("=", $route[$i], 2)[0];
39
+            $val = explode("=", $route[$i], 2)[1];
40
+            $VARS[$key] = $val;
41
+        }
42
+    }
43
+
44
+    if (strpos($route[count($route) - 1], "?") === 0) {
45
+        $morevars = explode("&", substr($route[count($route) - 1], 1));
46
+        foreach ($morevars as $var) {
47
+            $key = explode("=", $var, 2)[0];
48
+            $val = explode("=", $var, 2)[1];
49
+            $VARS[$key] = $val;
50
+        }
51
+    }
52
+}
53
+
54
+if (!authenticate()) {
55
+    header('WWW-Authenticate: Basic realm="' . $SETTINGS['site_title'] . '"');
56
+    header('HTTP/1.1 401 Unauthorized');
57
+    die("401 Unauthorized: you need to supply valid credentials.");
58
+}
59
+
60
+if (empty($VARS['action'])) {
61
+    http_response_code(404);
62
+    die("404 No action specified");
63
+}
64
+
65
+if (!isset($APIS[$VARS['action']])) {
66
+    http_response_code(404);
67
+    die("404 Action not defined");
68
+}
69
+
70
+$APIACTION = $APIS[$VARS["action"]];
71
+
72
+if (!file_exists(__DIR__ . "/actions/" . $APIACTION["load"])) {
73
+    http_response_code(404);
74
+    die("404 Action not found");
75
+}
76
+
77
+if (!empty($APIACTION["vars"])) {
78
+    checkVars($APIACTION["vars"]);
79
+}
80
+
81
+require_once __DIR__ . "/actions/" . $APIACTION["load"];

+ 34
- 28
app.php View File

@@ -1,5 +1,4 @@
1 1
 <?php
2
-
3 2
 /* This Source Code Form is subject to the terms of the Mozilla Public
4 3
  * License, v. 2.0. If a copy of the MPL was not distributed with this
5 4
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@@ -14,7 +13,7 @@ if ($_SESSION['loggedin'] != true) {
14 13
 require_once __DIR__ . "/pages.php";
15 14
 
16 15
 $pageid = "home";
17
-if (isset($_GET['page']) && !is_empty($_GET['page'])) {
16
+if (!empty($_GET['page'])) {
18 17
     $pg = strtolower($_GET['page']);
19 18
     $pg = preg_replace('/[^0-9a-z_]/', "", $pg);
20 19
     if (array_key_exists($pg, PAGES) && file_exists(__DIR__ . "/pages/" . $pg . ".php")) {
@@ -40,7 +39,7 @@ header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", fals
40 39
         <meta http-equiv="X-UA-Compatible" content="IE=edge">
41 40
         <meta name="viewport" content="width=device-width, initial-scale=1">
42 41
 
43
-        <title><?php echo SITE_TITLE; ?></title>
42
+        <title><?php echo $SETTINGS['site_title']; ?></title>
44 43
 
45 44
         <link rel="icon" href="static/img/logo.svg">
46 45
 
@@ -66,28 +65,35 @@ header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", fals
66 65
 
67 66
         <?php
68 67
 // Alert messages
69
-        if (isset($_GET['msg']) && !is_empty($_GET['msg']) && array_key_exists($_GET['msg'], MESSAGES)) {
70
-            // optional string generation argument
71
-            if (!isset($_GET['arg']) || is_empty($_GET['arg'])) {
72
-                $alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false);
68
+        if (!empty($_GET['msg'])) {
69
+            if (array_key_exists($_GET['msg'], MESSAGES)) {
70
+                // optional string generation argument
71
+                if (empty($_GET['arg'])) {
72
+                    $alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false);
73
+                } else {
74
+                    $alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
75
+                }
76
+                $alerttype = MESSAGES[$_GET['msg']]['type'];
77
+                $alerticon = "square-o";
78
+                switch (MESSAGES[$_GET['msg']]['type']) {
79
+                    case "danger":
80
+                        $alerticon = "times";
81
+                        break;
82
+                    case "warning":
83
+                        $alerticon = "exclamation-triangle";
84
+                        break;
85
+                    case "info":
86
+                        $alerticon = "info-circle";
87
+                        break;
88
+                    case "success":
89
+                        $alerticon = "check";
90
+                        break;
91
+                }
73 92
             } else {
74
-                $alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
75
-            }
76
-            $alerttype = MESSAGES[$_GET['msg']]['type'];
77
-            $alerticon = "square-o";
78
-            switch (MESSAGES[$_GET['msg']]['type']) {
79
-                case "danger":
80
-                    $alerticon = "times";
81
-                    break;
82
-                case "warning":
83
-                    $alerticon = "exclamation-triangle";
84
-                    break;
85
-                case "info":
86
-                    $alerticon = "info-circle";
87
-                    break;
88
-                case "success":
89
-                    $alerticon = "check";
90
-                    break;
93
+                // We don't have a message for this, so just assume an error and escape stuff.
94
+                $alertmsg = htmlentities($Strings->get($_GET['msg'], false));
95
+                $alerticon = "times";
96
+                $alerttype = "danger";
91 97
             }
92 98
             echo <<<END
93 99
             <div class="row justify-content-center" id="msg-alert-box">
@@ -121,7 +127,7 @@ END;
121 127
             </button>
122 128
             <a class="navbar-brand py-0 mr-auto" href="app.php">
123 129
                 <img src="static/img/logo.svg" alt="" class="d-none d-<?php echo $navbar_breakpoint; ?>-inline brand-img py-0" />
124
-                <?php echo SITE_TITLE; ?>
130
+                <?php echo $SETTINGS['site_title']; ?>
125 131
             </a>
126 132
 
127 133
             <div class="collapse navbar-collapse py-0" id="navbar-collapse">
@@ -157,7 +163,7 @@ END;
157 163
                 </div>
158 164
                 <div class="navbar-nav ml-auto py-0" id="navbar-right">
159 165
                     <span class="nav-item py-<?php echo $navbar_breakpoint; ?>-0">
160
-                        <a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="<?php echo PORTAL_URL; ?>">
166
+                        <a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="<?php echo $SETTINGS['accounthub']['home']; ?>">
161 167
                             <i class="fas fa-user fa-fw"></i><span>&nbsp;<?php echo $_SESSION['realname'] ?></span>
162 168
                         </a>
163 169
                     </span>
@@ -177,8 +183,8 @@ END;
177 183
                 ?>
178 184
             </div>
179 185
             <div class="footer">
180
-                <?php echo FOOTER_TEXT; ?><br />
181
-                Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
186
+                <?php echo $SETTINGS['footer_text']; ?><br />
187
+                Copyright &copy; <?php echo date('Y'); ?> <?php echo $SETTINGS['copyright']; ?>
182 188
             </div>
183 189
         </div>
184 190
         <script src="static/js/jquery-3.3.1.min.js"></script>

+ 111
- 155
index.php View File

@@ -1,175 +1,131 @@
1 1
 <?php
2
-/* This Source Code Form is subject to the terms of the Mozilla Public
2
+/*
3
+ * This Source Code Form is subject to the terms of the Mozilla Public
3 4
  * License, v. 2.0. If a copy of the MPL was not distributed with this
4
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
+ */
5 7
 
6 8
 require_once __DIR__ . "/required.php";
7 9
 
8 10
 // if we're logged in, we don't need to be here.
9 11
 if (!empty($_SESSION['loggedin']) && $_SESSION['loggedin'] === true && !isset($_GET['permissionerror'])) {
10 12
     header('Location: app.php');
13
+    die();
11 14
 }
12 15
 
13
-if (isset($_GET['permissionerror'])) {
14
-    $alert = $Strings->get("no access permission", false);
16
+/**
17
+ * Show a simple HTML page with a line of text and a button.  Matches the UI of
18
+ * the AccountHub login flow.
19
+ *
20
+ * @global type $SETTINGS
21
+ * @global type $SECURE_NONCE
22
+ * @global type $Strings
23
+ * @param string $title Text to show, passed through i18n
24
+ * @param string $button Button text, passed through i18n
25
+ * @param string $url URL for the button
26
+ */
27
+function showHTML(string $title, string $button, string $url) {
28
+    global $SETTINGS, $SECURE_NONCE, $Strings;
29
+    ?>
30
+    <!DOCTYPE html>
31
+    <meta charset="UTF-8">
32
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
33
+    <meta name="viewport" content="width=device-width, initial-scale=1">
34
+
35
+    <title><?php echo $SETTINGS['site_title']; ?></title>
36
+
37
+    <link rel="icon" href="static/img/logo.svg">
38
+
39
+    <link href="static/css/bootstrap.min.css" rel="stylesheet">
40
+    <style nonce="<?php echo $SECURE_NONCE; ?>">
41
+        .display-5 {
42
+            font-size: 2.5rem;
43
+            font-weight: 300;
44
+            line-height: 1.2;
45
+        }
46
+
47
+        .banner-image {
48
+            max-height: 100px;
49
+            margin: 2em auto;
50
+            border: 1px solid grey;
51
+            border-radius: 15%;
52
+        }
53
+    </style>
54
+
55
+    <div class="container mt-4">
56
+        <div class="row justify-content-center">
57
+            <div class="col-12 text-center">
58
+                <img class="banner-image" src="./static/img/logo.svg" />
59
+            </div>
60
+
61
+            <div class="col-12 text-center">
62
+                <h1 class="display-5 mb-4"><?php $Strings->get($title); ?></h1>
63
+            </div>
64
+
65
+            <div class="col-12 col-sm-8 col-lg-6">
66
+                <div class="card mt-4">
67
+                    <div class="card-body">
68
+                        <a href="<?php echo $url; ?>" class="btn btn-primary btn-block"><?php $Strings->get($button); ?></a>
69
+                    </div>
70
+                </div>
71
+            </div>
72
+        </div>
73
+    </div>
74
+    <?php
15 75
 }
16 76
 
17
-/* Authenticate user */
18
-$userpass_ok = false;
19
-$multiauth = false;
20
-if (Login::checkLoginServer()) {
21
-    if (empty($VARS['progress'])) {
22
-        // Easy way to remove "undefined" warnings.
23
-    } else if ($VARS['progress'] == "1") {
24
-        if (!CAPTCHA_ENABLED || (CAPTCHA_ENABLED && Login::verifyCaptcha($VARS['captcheck_session_code'], $VARS['captcheck_selected_answer'], CAPTCHA_SERVER . "/api.php"))) {
25
-            $autherror = "";
26
-            $user = User::byUsername($VARS['username']);
27
-            if ($user->exists()) {
28
-                $status = $user->getStatus()->getString();
29
-                switch ($status) {
30
-                    case "LOCKED_OR_DISABLED":
31
-                        $alert = $Strings->get("account locked", false);
32
-                        break;
33
-                    case "TERMINATED":
34
-                        $alert = $Strings->get("account terminated", false);
35
-                        break;
36
-                    case "CHANGE_PASSWORD":
37
-                        $alert = $Strings->get("password expired", false);
38
-                        break;
39
-                    case "NORMAL":
40
-                        $username_ok = true;
41
-                        break;
42
-                    case "ALERT_ON_ACCESS":
43
-                        $mail_resp = $user->sendAlertEmail();
44
-                        if (DEBUG) {
45
-                            var_dump($mail_resp);
46
-                        }
47
-                        $username_ok = true;
48
-                        break;
49
-                    default:
50
-                        if (!is_empty($error)) {
51
-                            $alert = $error;
52
-                        } else {
53
-                            $alert = $Strings->get("login error", false);
54
-                        }
55
-                        break;
56
-                }
57
-                if ($username_ok) {
58
-                    if ($user->checkPassword($VARS['password'])) {
59
-                        $_SESSION['passok'] = true; // stop logins using only username and authcode
60
-                        if ($user->has2fa()) {
61
-                            $multiauth = true;
62
-                        } else {
63
-                            Session::start($user);
64
-                            header('Location: app.php');
65
-                            die("Logged in, go to app.php");
66
-                        }
67
-                    } else {
68
-                        $alert = $Strings->get("login incorrect", false);
69
-                    }
77
+if (!empty($_GET['logout'])) {
78
+    showHTML("You have been logged out.", "Log in again", "./index.php");
79
+    die();
80
+}
81
+if (empty($_SESSION["login_code"])) {
82
+    $redirecttologin = true;
83
+} else {
84
+    try {
85
+        $uidinfo = AccountHubApi::get("checkloginkey", ["code" => $_SESSION["login_code"]]);
86
+        if ($uidinfo["status"] == "ERROR") {
87
+            throw new Exception();
88
+        }
89
+        if (is_numeric($uidinfo['uid'])) {
90
+            $user = new User($uidinfo['uid'] * 1);
91
+            foreach ($SETTINGS['permissions'] as $perm) {
92
+                if (!$user->hasPermission($perm)) {
93
+                    showHTML("no access permission", "sign out", "./action.php?action=signout");
94
+                    die();
70 95
                 }
71
-            } else { // User does not exist anywhere
72
-                $alert = $Strings->get("login incorrect", false);
73 96
             }
74
-        } else {
75
-            $alert = $Strings->get("captcha error", false);
76
-        }
77
-    } else if ($VARS['progress'] == "2") {
78
-        $user = User::byUsername($VARS['username']);
79
-        if ($_SESSION['passok'] !== true) {
80
-            // stop logins using only username and authcode
81
-            sendError("Password integrity check failed!");
82
-        }
83
-        if ($user->check2fa($VARS['authcode'])) {
84 97
             Session::start($user);
98
+            $_SESSION["login_code"] = null;
85 99
             header('Location: app.php');
86
-            die("Logged in, go to app.php");
100
+            showHTML("Logged in", "Continue", "./app.php");
101
+            die();
87 102
         } else {
88
-            $alert = $Strings->get("2fa incorrect", false);
103
+            throw new Exception();
89 104
         }
105
+    } catch (Exception $ex) {
106
+        $redirecttologin = true;
90 107
     }
91
-} else {
92
-    $alert = $Strings->get("login server unavailable", false);
93 108
 }
94
-header("Link: <static/fonts/Roboto.css>; rel=preload; as=style", false);
95
-header("Link: <static/css/bootstrap.min.css>; rel=preload; as=style", false);
96
-header("Link: <static/css/material-color/material-color.min.css>; rel=preload; as=style", false);
97
-header("Link: <static/css/index.css>; rel=preload; as=style", false);
98
-header("Link: <static/js/jquery-3.3.1.min.js>; rel=preload; as=script", false);
99
-header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", false);
100
-?>
101
-<!DOCTYPE html>
102
-<html>
103
-    <head>
104
-        <meta charset="UTF-8">
105
-        <meta http-equiv="X-UA-Compatible" content="IE=edge">
106
-        <meta name="viewport" content="width=device-width, initial-scale=1">
107
-
108
-        <title><?php echo SITE_TITLE; ?></title>
109
-
110
-        <link rel="icon" href="static/img/logo.svg">
111
-
112
-        <link href="static/css/bootstrap.min.css" rel="stylesheet">
113
-        <link href="static/css/material-color/material-color.min.css" rel="stylesheet">
114
-        <link href="static/css/index.css" rel="stylesheet">
115
-        <?php if (CAPTCHA_ENABLED) { ?>
116
-            <script src="<?php echo CAPTCHA_SERVER ?>/captcheck.dist.js"></script>
117
-        <?php } ?>
118
-    </head>
119
-    <body>
120
-        <div class="row justify-content-center">
121
-            <div class="col-auto">
122
-                <img class="banner-image" src="static/img/logo.svg" />
123
-            </div>
124
-        </div>
125
-        <div class="row justify-content-center">
126
-            <div class="card col-11 col-xs-11 col-sm-8 col-md-6 col-lg-4">
127
-                <div class="card-body">
128
-                    <h5 class="card-title"><?php $Strings->get("sign in"); ?></h5>
129
-                    <form action="" method="POST">
130
-                        <?php
131
-                        if (!empty($alert)) {
132
-                            ?>
133
-                            <div class="alert alert-danger">
134
-                                <i class="fa fa-fw fa-exclamation-triangle"></i> <?php echo $alert; ?>
135
-                            </div>
136
-                            <?php
137
-                        }
138
-
139
-                        if ($multiauth != true) {
140
-                            ?>
141
-                            <input type="text" class="form-control" name="username" placeholder="<?php $Strings->get("username"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
142
-                            <input type="password" class="form-control" name="password" placeholder="<?php $Strings->get("password"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
143
-                            <?php if (CAPTCHA_ENABLED) { ?>
144
-                                <div class="captcheck_container" data-stylenonce="<?php echo $SECURE_NONCE; ?>"></div>
145
-                                <br />
146
-                            <?php } ?>
147
-                            <input type="hidden" name="progress" value="1" />
148
-                            <?php
149
-                        } else if ($multiauth) {
150
-                            ?>
151
-                            <div class="alert alert-info">
152
-                                <?php $Strings->get("2fa prompt"); ?>
153
-                            </div>
154
-                            <input type="text" class="form-control" name="authcode" placeholder="<?php $Strings->get("authcode"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
155
-                            <input type="hidden" name="progress" value="2" />
156
-                            <input type="hidden" name="username" value="<?php echo $VARS['username']; ?>" />
157
-                            <?php
158
-                        }
159
-                        ?>
160
-                        <button type="submit" class="btn btn-primary">
161
-                            <?php $Strings->get("continue"); ?>
162
-                        </button>
163
-                    </form>
164
-                </div>
165
-            </div>
166
-        </div>
167
-        <div class="footer">
168
-            <?php echo FOOTER_TEXT; ?><br />
169
-            Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
170
-        </div>
171
-    </div>
172
-    <script src="static/js/jquery-3.3.1.min.js"></script>
173
-    <script src="static/js/bootstrap.bundle.min.js"></script>
174
-</body>
175
-</html>
109
+
110
+if ($redirecttologin) {
111
+    try {
112
+        $urlbase = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . (($_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) ? ":" . $_SERVER['SERVER_PORT'] : "");
113
+        $iconurl = $urlbase . str_replace("index.php", "", $_SERVER["REQUEST_URI"]) . "static/img/logo.svg";
114
+        $codedata = AccountHubApi::get("getloginkey", ["appname" => $SETTINGS["site_title"], "appicon" => $iconurl]);
115
+
116
+        if ($codedata['status'] != "OK") {
117
+            throw new Exception($Strings->get("login server unavailable", false));
118
+        }
119
+
120
+        $redirecturl = $urlbase . $_SERVER['REQUEST_URI'];
121
+
122
+        $_SESSION["login_code"] = $codedata["code"];
123
+
124
+        $locationurl = $codedata["loginurl"] . "?code=" . htmlentities($codedata["code"]) . "&redirect=" . htmlentities($redirecturl);
125
+        header("Location: $locationurl");
126
+        showHTML("Continue", "Continue", $locationurl);
127
+        die();
128
+    } catch (Exception $ex) {
129
+        sendError($ex->getMessage());
130
+    }
131
+}

+ 1
- 20
langs/en/core.json View File

@@ -1,26 +1,7 @@
1 1
 {
2
-    "sign in": "Sign In",
3
-    "username": "Username",
4
-    "password": "Password",
5
-    "continue": "Continue",
6
-    "authcode": "Authentication code",
7
-    "2fa prompt": "Enter the six-digit code from your mobile authenticator app.",
8
-    "2fa incorrect": "Authentication code incorrect.",
9
-    "login incorrect": "Login incorrect.",
10
-    "login server unavailable": "Login server unavailable.  Try again later or contact technical support.",
11
-    "account locked": "This account has been disabled. Contact technical support.",
12
-    "password expired": "You must change your password before continuing.",
13
-    "account terminated": "Account terminated.  Access denied.",
14
-    "account state error": "Your account state is not stable.  Log out, restart your browser, and try again.",
15
-    "welcome user": "Welcome, {user}!",
16 2
     "sign out": "Sign out",
17
-    "settings": "Settings",
18
-    "options": "Options",
19 3
     "404 error": "404 Error",
20 4
     "page not found": "Page not found.",
21 5
     "invalid parameters": "Invalid request parameters.",
22
-    "login server error": "The login server returned an error: {arg}",
23
-    "login server user data error": "The login server refused to provide account information.  Try again or contact technical support.",
24
-    "captcha error": "There was a problem with the CAPTCHA (robot test).  Try again.",
25
-    "no access permission": "You do not have permission to access this system."
6
+    "login server error": "The login server returned an error: {arg}"
26 7
 }

+ 8
- 0
langs/en/index.json View File

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

+ 2
- 2
langs/en/titles.json View File

@@ -1,4 +1,4 @@
1 1
 {
2
-    "home": "Home",
3
-    "test": "Test"
2
+    "Home": "Home",
3
+    "Form": "Form"
4 4
 }

+ 4
- 0
langs/messages.php View File

@@ -41,6 +41,10 @@ define("MESSAGES", [
41 41
         "string" => "cash opened",
42 42
         "type" => "success"
43 43
     ],
44
+    "cash_closed" => [
45
+        "string" => "cash closed",
46
+        "type" => "success"
47
+    ],
44 48
     "register_saved" => [
45 49
         "string" => "register saved",
46 50
         "type" => "success"

+ 56
- 0
lib/AccountHubApi.lib.php View File

@@ -0,0 +1,56 @@
1
+<?php
2
+
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+class AccountHubApi {
10
+
11
+    public static function get(string $action, array $data = null, bool $throwex = false) {
12
+        global $SETTINGS;
13
+
14
+        $content = [
15
+            "action" => $action,
16
+            "key" => $SETTINGS['accounthub']['key']
17
+        ];
18
+        if (!is_null($data)) {
19
+            $content = array_merge($content, $data);
20
+        }
21
+        $options = [
22
+            'http' => [
23
+                'method' => 'POST',
24
+                'content' => json_encode($content),
25
+                'header' => "Content-Type: application/json\r\n" .
26
+                "Accept: application/json\r\n",
27
+                "ignore_errors" => true
28
+            ]
29
+        ];
30
+
31
+        $context = stream_context_create($options);
32
+        $result = file_get_contents($SETTINGS['accounthub']['api'], false, $context);
33
+        $response = json_decode($result, true);
34
+        if ($result === false || !AccountHubApi::checkHttpRespCode($http_response_header) || json_last_error() != JSON_ERROR_NONE) {
35
+            if ($throwex) {
36
+                throw new Exception($result);
37
+            } else {
38
+                sendError($result);
39
+            }
40
+        }
41
+        return $response;
42
+    }
43
+
44
+    private static function checkHttpRespCode(array $headers): bool {
45
+        foreach ($headers as $header) {
46
+            if (preg_match("/HTTP\/[0-9]\.[0-9] [0-9]{3}.*/", $header)) {
47
+                $respcode = explode(" ", $header)[1] * 1;
48
+                if ($respcode >= 200 && $respcode < 300) {
49
+                    return true;
50
+                }
51
+            }
52
+        }
53
+        return false;
54
+    }
55
+
56
+}

+ 326
- 0
lib/FormBuilder.lib.php View File

@@ -0,0 +1,326 @@
1
+<?php
2
+
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+class FormBuilder {
10
+
11
+    private $items = [];
12
+    private $hiddenitems = [];
13
+    private $title = "";
14
+    private $icon = "";
15
+    private $buttons = [];
16
+    private $action = "action.php";
17
+    private $method = "POST";
18
+    private $id = "editform";
19
+
20
+    /**
21
+     * Create a form with autogenerated HTML.
22
+     *
23
+     * @param string $title Form title/heading
24
+     * @param string $icon FontAwesone icon next to the title.
25
+     * @param string $action URL to submit the form to.
26
+     * @param string $method Form submission method (POST, GET, etc.)
27
+     */
28
+    public function __construct(string $title = "Untitled Form", string $icon = "fas fa-file-alt", string $action = "action.php", string $method = "POST") {
29
+        $this->title = $title;
30
+        $this->icon = $icon;
31
+        $this->action = $action;
32
+        $this->method = $method;
33
+    }
34
+
35
+    /**
36
+     * Set the title of the form.
37
+     * @param string $title
38
+     */
39
+    public function setTitle(string $title) {
40
+        $this->title = $title;
41
+    }
42
+
43
+    /**
44
+     * Set the icon for the form.
45
+     * @param string $icon FontAwesome icon (example: "fas fa-toilet-paper")
46
+     */
47
+    public function setIcon(string $icon) {
48
+        $this->icon = $icon;
49
+    }
50
+
51
+    /**
52
+     * Set the URL the form will submit to.
53
+     * @param string $action
54
+     */
55
+    public function setAction(string $action) {
56
+        $this->action = $action;
57
+    }
58
+
59
+    /**
60
+     * Set the form submission method (GET, POST, etc)
61
+     * @param string $method
62
+     */
63
+    public function setMethod(string $method = "POST") {
64
+        $this->method = $method;
65
+    }
66
+
67
+    /**
68
+     * Set the form ID.
69
+     * @param string $id
70
+     */
71
+    public function setID(string $id = "editform") {
72
+        $this->id = $id;
73
+    }
74
+
75
+    /**
76
+     * Add an input to the form.
77
+     *
78
+     * @param string $name Element name
79
+     * @param string $value Element value
80
+     * @param string $type Input type (text, number, date, select, tel...)
81
+     * @param bool $required If the element is required for form submission.
82
+     * @param string $id Element ID
83
+     * @param array $options Array of [value => text] pairs for a select element
84
+     * @param string $label Text label to display near the input
85
+     * @param string $icon FontAwesome icon (example: "fas fa-toilet-paper")
86
+     * @param int $width Bootstrap column width for the input, out of 12.
87
+     * @param int $minlength Minimum number of characters for the input.
88
+     * @param int $maxlength Maximum number of characters for the input.
89
+     * @param string $pattern Regex pattern for custom client-side validation.
90
+     * @param string $error Message to show if the input doesn't validate.
91
+     */
92
+    public function addInput(string $name, string $value = "", string $type = "text", bool $required = true, string $id = null, array $options = null, string $label = "", string $icon = "", int $width = 4, int $minlength = 1, int $maxlength = 100, string $pattern = "", string $error = "") {
93
+        $item = [
94
+            "name" => $name,
95
+            "value" => $value,
96
+            "type" => $type,
97
+            "required" => $required,
98
+            "label" => $label,
99
+            "icon" => $icon,
100
+            "width" => $width,
101
+            "minlength" => $minlength,
102
+            "maxlength" => $maxlength
103
+        ];
104
+        if (!empty($id)) {
105
+            $item["id"] = $id;
106
+        }
107
+        if (!empty($options) && $type == "select") {
108
+            $item["options"] = $options;
109
+        }
110
+        if (!empty($pattern)) {
111
+            $item["pattern"] = $pattern;
112
+        }
113
+        if (!empty($error)) {
114
+            $item["error"] = $error;
115
+        }
116
+        $this->items[] = $item;
117
+    }
118
+
119
+    /**
120
+     * Add a text input.
121
+     *
122
+     * @param string $name Element name
123
+     * @param string $value Element value
124
+     * @param bool $required If the element is required for form submission.
125
+     * @param string $id Element ID
126
+     * @param string $label Text label to display near the input
127
+     * @param string $icon FontAwesome icon (example: "fas fa-toilet-paper")
128
+     * @param int $width Bootstrap column width for the input, out of 12.
129
+     * @param int $minlength Minimum number of characters for the input.
130
+     * @param int $maxlength Maximum number of characters for the input.
131
+     * @param string $pattern Regex pattern for custom client-side validation.
132
+     * @param string $error Message to show if the input doesn't validate.
133
+     */
134
+    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 = "") {
135
+        $this->addInput($name, $value, "text", $required, $id, null, $label, $icon, $width, $minlength, $maxlength, $pattern, $error);
136
+    }
137
+
138
+    /**
139
+     * Add a select dropdown.
140
+     *
141
+     * @param string $name Element name
142
+     * @param string $value Element value
143
+     * @param bool $required If the element is required for form submission.
144
+     * @param string $id Element ID
145
+     * @param array $options Array of [value => text] pairs for a select element
146
+     * @param string $label Text label to display near the input
147
+     * @param string $icon FontAwesome icon (example: "fas fa-toilet-paper")
148
+     * @param int $width Bootstrap column width for the input, out of 12.
149
+     */
150
+    public function addSelect(string $name, string $value = "", bool $required = true, string $id = null, array $options = null, string $label = "", string $icon = "", int $width = 4) {
151
+        $this->addInput($name, $value, "select", $required, $id, $options, $label, $icon, $width);
152
+    }
153
+
154
+    /**
155
+     * Add a button to the form.
156
+     *
157
+     * @param string $text Text string to show on the button.
158
+     * @param string $icon FontAwesome icon to show next to the text.
159
+     * @param string $href If not null, the button will actually be a hyperlink.
160
+     * @param string $type Usually "button" or "submit".  Ignored if $href is set.
161
+     * @param string $id The element ID.
162
+     * @param string $name The element name for the button.
163
+     * @param string $value The form value for the button. Ignored if $name is null.
164
+     * @param string $class The CSS classes for the button, if a standard success-colored one isn't right.
165
+     */
166
+    public function addButton(string $text, string $icon = "", string $href = null, string $type = "button", string $id = null, string $name = null, string $value = "", string $class = "btn btn-success") {
167
+        $button = [
168
+            "text" => $text,
169
+            "icon" => $icon,
170
+            "class" => $class,
171
+            "type" => $type,
172
+            "id" => $id,
173
+            "href" => $href,
174
+            "name" => $name,
175
+            "value" => $value
176
+        ];
177
+        $this->buttons[] = $button;
178
+    }
179
+
180
+    /**
181
+     * Add a hidden input.
182
+     * @param string $name
183
+     * @param string $value
184
+     */
185
+    public function addHiddenInput(string $name, string $value) {
186
+        $this->hiddenitems[$name] = $value;
187
+    }
188
+
189
+    /**
190
+     * Generate the form HTML.
191
+     * @param bool $echo If false, returns HTML string instead of outputting it.
192
+     */
193
+    public function generate(bool $echo = true) {
194
+        $html = <<<HTMLTOP
195
+<form action="$this->action" method="$this->method" id="$this->id">
196
+    <div class="card">
197
+         <h3 class="card-header d-flex">
198
+            <div>
199
+                <i class="$this->icon"></i> $this->title
200
+            </div>
201
+        </h3>
202
+
203
+        <div class="card-body">
204
+            <div class="row">
205
+HTMLTOP;
206
+
207
+        foreach ($this->items as $item) {
208
+            $required = $item["required"] ? "required" : "";
209
+            $id = empty($item["id"]) ? "" : "id=\"$item[id]\"";
210
+            $pattern = empty($item["pattern"]) ? "" : "pattern=\"$item[pattern]\"";
211
+            if (empty($item['type'])) {
212
+                $item['type'] = "text";
213
+            }
214
+            $itemhtml = "";
215
+            $itemlabel = "";
216
+
217
+            if ($item['type'] == "textarea") {
218
+                $itemlabel = "<label class=\"mb-0\"><i class=\"$item[icon]\"></i> $item[label]:</label>";
219
+            } else if ($item['type'] != "checkbox") {
220
+                $itemlabel = "<label class=\"mb-0\">$item[label]:</label>";
221
+            }
222
+            $strippedlabel = strip_tags($item['label']);
223
+            $itemhtml .= <<<ITEMTOP
224
+\n\n                <div class="col-12 col-md-$item[width]">
225
+                    <div class="form-group mb-3">
226
+                        $itemlabel
227
+ITEMTOP;
228
+            $inputgrouptop = <<<INPUTG
229
+\n                            <div class="input-group">
230
+                            <div class="input-group-prepend">
231
+                                <span class="input-group-text"><i class="$item[icon]"></i></span>
232
+                            </div>
233
+INPUTG;
234
+            switch ($item['type']) {
235
+                case "select":
236
+                    $itemhtml .= $inputgrouptop;
237
+                    $itemhtml .= <<<SELECT
238
+\n                            <select class="form-control" name="$item[name]" aria-label="$strippedlabel" $required>
239
+SELECT;
240
+                    foreach ($item['options'] as $value => $label) {
241
+                        $selected = "";
242
+                        if (!empty($item['value']) && $value == $item['value']) {
243
+                            $selected = " selected";
244
+                        }
245
+                        $itemhtml .= "\n                                <option value=\"$value\"$selected>$label</option>";
246
+                    }
247
+                    $itemhtml .= "\n                            </select>";
248
+                    break;
249
+                case "checkbox":
250
+                    $itemhtml .= $inputgrouptop;
251
+                    $itemhtml .= <<<CHECKBOX
252
+\n                            <div class="form-group form-check">
253
+                                <input type="checkbox" name="$item[name]" $id class="form-check-input" value="$item[value]" $required aria-label="$strippedlabel">
254
+                                <label class="form-check-label">$item[label]</label>
255
+                              </div>
256
+CHECKBOX;
257
+                    break;
258
+                case "textarea":
259
+                    $val = htmlentities($item['value']);
260
+                    $itemhtml .= <<<TEXTAREA
261
+\n                            <textarea class="form-control" id="info" name="$item[name]" aria-label="$strippedlabel" minlength="$item[minlength]" maxlength="$item[maxlength]" $required>$val</textarea>
262
+TEXTAREA;
263
+                    break;
264
+                default:
265
+                    $itemhtml .= $inputgrouptop;
266
+                    $itemhtml .= <<<INPUT
267
+\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 />
268
+INPUT;
269
+                    break;
270
+            }
271
+
272
+            if (!empty($item["error"])) {
273
+                $itemhtml .= <<<ERROR
274
+\n                            <div class="invalid-feedback">
275
+                                $item[error]
276
+                            </div>
277
+ERROR;
278
+            }
279
+            if ($item["type"] != "textarea") {
280
+                $itemhtml .= "\n                                </div>";
281
+            }
282
+            $itemhtml .= <<<ITEMBOTTOM
283
+\n                    </div>
284
+                </div>\n
285
+ITEMBOTTOM;
286
+            $html .= $itemhtml;
287
+        }
288
+
289
+        $html .= <<<HTMLBOTTOM
290
+
291
+            </div>
292
+        </div>
293
+HTMLBOTTOM;
294
+
295
+        if (!empty($this->buttons)) {
296
+            $html .= "\n        <div class=\"card-footer d-flex\">";
297
+            foreach ($this->buttons as $btn) {
298
+                $btnhtml = "";
299
+                $inner = "<i class=\"$btn[icon]\"></i> $btn[text]";
300
+                $id = empty($btn['id']) ? "" : "id=\"$btn[id]\"";
301
+                if (!empty($btn['href'])) {
302
+                    $btnhtml = "<a href=\"$btn[href]\" class=\"$btn[class]\" $id>$inner</a>";
303
+                } else {
304
+                    $name = empty($btn['name']) ? "" : "name=\"$btn[name]\"";
305
+                    $value = (!empty($btn['name']) && !empty($btn['value'])) ? "value=\"$btn[value]\"" : "";
306
+                    $btnhtml = "<button type=\"$btn[type]\" class=\"$btn[class]\" $id $name $value>$inner</button>";
307
+                }
308
+                $html .= "\n            $btnhtml";
309
+            }
310
+            $html .= "\n        </div>";
311
+        }
312
+
313
+        $html .= "\n    </div>";
314
+        foreach ($this->hiddenitems as $name => $value) {
315
+            $value = htmlentities($value);
316
+            $html .= "\n    <input type=\"hidden\" name=\"$name\" value=\"$value\" />";
317
+        }
318
+        $html .= "\n</form>\n";
319
+
320
+        if ($echo) {
321
+            echo $html;
322
+        }
323
+        return $html;
324
+    }
325
+
326
+}

+ 13
- 13
lib/GenerateReceipt.lib.php View File

@@ -13,14 +13,14 @@ class GenerateReceipt {
13 13
     const RECEIPT_TYPE_Z = 3;
14 14
 
15 15
     private static function saleReceipt($transaction) {
16
-        global $database;
16
+        global $database, $Strings, $SETTINGS;
17 17
         $receipt = new Receipt();
18 18
         $tx = $database->get('transactions', ['txid', 'txdate', 'customerid', 'type', 'cashier', 'discountpercent'], ['txid' => $transaction]);
19 19
         // Info
20 20
         $txid = $tx['txid'];
21
-        $datetime = date(DATETIME_FORMAT, strtotime($tx['txdate']));
21
+        $datetime = date($SETTINGS['datetime_format'], strtotime($tx['txdate']));
22 22
         $type = $tx['type'];
23
-        $cashier = getUserByID($tx['cashier'])['name'];
23
+        $cashier = (new User($tx['cashier']))->getName();
24 24
         $customerid = $tx['customerid'];
25 25
 
26 26
         // Items
@@ -89,14 +89,14 @@ class GenerateReceipt {
89 89
     }
90 90
 
91 91
     private static function returnReceipt($transaction) {
92
-        global $database;
92
+        global $database, $Strings, $SETTINGS;
93 93
         $receipt = new Receipt();
94 94
         $tx = $database->get('transactions', ['txid', 'txdate', 'customerid', 'type', 'cashier', 'discountpercent'], ['txid' => $transaction]);
95 95
         // Info
96 96
         $txid = $tx['txid'];
97
-        $datetime = date(DATETIME_FORMAT, strtotime($tx['txdate']));
97
+        $datetime = date($SETTINGS['datetime_format'], strtotime($tx['txdate']));
98 98
         $type = $tx['type'];
99
-        $cashier = getUserByID($tx['cashier'])['name'];
99
+        $cashier = (new User($tx['cashier']))->getName();
100 100
         $customerid = $tx['customerid'];
101 101
 
102 102
         // Items
@@ -165,7 +165,7 @@ class GenerateReceipt {
165 165
     }
166 166
 
167 167
     static function xReceipt($registerid) {
168
-        global $database;
168
+        global $database, $Strings, $SETTINGS;
169 169
         $receipt = new Receipt();
170 170
 
171 171
         $registername = $database->get('registers', 'registername', ['registerid' => $registerid]);
@@ -191,7 +191,7 @@ class GenerateReceipt {
191 191
 
192 192
         $receipt->appendHeader(new ReceiptLine($Strings->get("x report", false), "", "", ReceiptLine::LINEFORMAT_BOLD | ReceiptLine::LINEFORMAT_CENTER));
193 193
 
194
-        $receipt->appendLine(new ReceiptLine("Printed:", "", date(DATETIME_FORMAT)));
194
+        $receipt->appendLine(new ReceiptLine("Printed:", "", date($SETTINGS['datetime_format'])));
195 195
         $receipt->appendLine(new ReceiptLine("Register:", "", $registername));
196 196
         $receipt->appendLine(new ReceiptLine("Transactions:", "", $transactioncount));
197 197
 
@@ -199,7 +199,7 @@ class GenerateReceipt {
199 199
         $receipt->appendBreak();
200 200
         $receipt->appendLine(new ReceiptLine("Opening", "", "", ReceiptLine::LINEFORMAT_CENTER));
201 201
         $receipt->appendBreak();
202
-        $receipt->appendLine(new ReceiptLine("Date:", "", date(DATETIME_FORMAT, strtotime($cash['open']))));
202
+        $receipt->appendLine(new ReceiptLine("Date:", "", date($SETTINGS['datetime_format'], strtotime($cash['open']))));
203 203
         $receipt->appendLine(new ReceiptLine("Cash:", "", '$' . number_format($cash['start_amount'], 2)));
204 204
 
205 205
         $receipt->appendBlank();
@@ -220,7 +220,7 @@ class GenerateReceipt {
220 220
     }
221 221
 
222 222
     static function zReceipt($cashid) {
223
-        global $database;
223
+        global $database, $Strings, $SETTINGS;
224 224
         $receipt = new Receipt();
225 225
 
226 226
         $cash = $database->get('cash_drawer', ['open', 'close', 'start_amount', 'end_amount', 'cashid', 'registerid'], ['cashid' => $cashid]);
@@ -246,7 +246,7 @@ class GenerateReceipt {
246 246
 
247 247
         $receipt->appendHeader(new ReceiptLine($Strings->get("z report", false), "", "", ReceiptLine::LINEFORMAT_BOLD | ReceiptLine::LINEFORMAT_CENTER));
248 248
 
249
-        $receipt->appendLine(new ReceiptLine("Printed:", "", date(DATETIME_FORMAT)));
249
+        $receipt->appendLine(new ReceiptLine("Printed:", "", date($SETTINGS['datetime_format'])));
250 250
         $receipt->appendLine(new ReceiptLine("Register:", "", $registername));
251 251
         $receipt->appendLine(new ReceiptLine("Transactions:", "", $transactioncount));
252 252
 
@@ -254,14 +254,14 @@ class GenerateReceipt {
254 254
         $receipt->appendBreak();
255 255
         $receipt->appendLine(new ReceiptLine("Opening", "", "", ReceiptLine::LINEFORMAT_CENTER));
256 256
         $receipt->appendBreak();
257
-        $receipt->appendLine(new ReceiptLine("Date:", "", date(DATETIME_FORMAT, strtotime($cash['open']))));
257
+        $receipt->appendLine(new ReceiptLine("Date:", "", date($SETTINGS['datetime_format'], strtotime($cash['open']))));
258 258
         $receipt->appendLine(new ReceiptLine("Cash:", "", '$' . number_format($cash['start_amount'], 2)));
259 259
 
260 260
         $receipt->appendBlank();
261 261
         $receipt->appendBreak();
262 262
         $receipt->appendLine(new ReceiptLine("Closing", "", "", ReceiptLine::LINEFORMAT_CENTER));
263 263
         $receipt->appendBreak();
264
-        $receipt->appendLine(new ReceiptLine("Date:", "", date(DATETIME_FORMAT, strtotime($cash['close']))));
264
+        $receipt->appendLine(new ReceiptLine("Date:", "", date($SETTINGS['datetime_format'], strtotime($cash['close']))));
265 265
         $receipt->appendLine(new ReceiptLine("Cash:", "", '$' . number_format($cash['end_amount'], 2)));
266 266
 
267 267
         $receipt->appendBlank();

+ 2
- 51
lib/Login.lib.php View File

@@ -45,50 +45,13 @@ class Login {
45 45
         return Login::LOGIN_OK;
46 46
     }
47 47
 
48
-    public static function verifyCaptcha(string $session, string $answer, string $url): bool {
49
-        $data = [
50
-            'session_id' => $session,
51
-            'answer_id' => $answer,
52
-            'action' => "verify"
53
-        ];
54
-        $options = [
55
-            'http' => [
56
-                'header' => "Content-type: application/x-www-form-urlencoded\r\n",
57
-                'method' => 'POST',
58
-                'content' => http_build_query($data)
59
-            ]
60
-        ];
61
-        $context = stream_context_create($options);
62
-        $result = file_get_contents($url, false, $context);
63
-        $resp = json_decode($result, TRUE);
64
-        if (!$resp['result']) {
65
-            return false;
66
-        } else {
67
-            return true;
68
-        }
69
-    }
70
-
71 48
     /**
72 49
      * Check the login server API for sanity
73 50
      * @return boolean true if OK, else false
74 51
      */
75 52
     public static function checkLoginServer() {
76 53
         try {
77
-            $client = new GuzzleHttp\Client();
78
-
79
-            $response = $client
80
-                    ->request('POST', PORTAL_API, [
81
-                'form_params' => [
82
-                    'key' => PORTAL_KEY,
83
-                    'action' => "ping"
84
-                ]
85
-            ]);
86
-
87
-            if ($response->getStatusCode() != 200) {
88
-                return false;
89
-            }
90
-
91
-            $resp = json_decode($response->getBody(), TRUE);
54
+            $resp = AccountHubApi::get("ping");
92 55
             if ($resp['status'] == "OK") {
93 56
                 return true;
94 57
             } else {
@@ -107,19 +70,7 @@ class Login {
107 70
      */
108 71
     function checkAPIKey($key) {
109 72
         try {
110
-            $client = new GuzzleHttp\Client();
111
-
112
-            $response = $client
113
-                    ->request('POST', PORTAL_API, [
114
-                'form_params' => [
115
-                    'key' => $key,
116
-                    'action' => "ping"
117
-                ]
118
-            ]);
119
-
120
-            if ($response->getStatusCode() === 200) {
121
-                return true;
122
-            }
73
+            $resp = AccountHubApi::get("ping", null, true);
123 74
             return false;
124 75
         } catch (Exception $e) {
125 76
             return false;

+ 9
- 21
lib/Notifications.lib.php View File

@@ -32,27 +32,15 @@ class Notifications {
32 32
                 $timestamp = date("Y-m-d H:i:s", strtotime($timestamp));
33 33
             }
34 34
 
35
-            $client = new GuzzleHttp\Client();
36
-
37
-            $response = $client
38
-                    ->request('POST', PORTAL_API, [
39
-                'form_params' => [
40
-                    'key' => PORTAL_KEY,
41
-                    'action' => "addnotification",
42
-                    'uid' => $user->getUID(),
43
-                    'title' => $title,
44
-                    'content' => $content,
45
-                    'timestamp' => $timestamp,
46
-                    'url' => $url,
47
-                    'sensitive' => $sensitive
48
-                ]
49
-            ]);
50
-
51
-            if ($response->getStatusCode() > 299) {
52
-                sendError("Login server error: " . $response->getBody());
53
-            }
54
-
55
-            $resp = json_decode($response->getBody(), TRUE);
35
+            $resp = AccountHubApi::get("addnotification", [
36
+                        'uid' => $user->getUID(),
37
+                        'title' => $title,
38
+                        'content' => $content,
39
+                        'timestamp' => $timestamp,
40
+                        'url' => $url,
41
+                        'sensitive' => $sensitive
42
+                            ]
43
+            );
56 44
             if ($resp['status'] == "OK") {
57 45
                 return $resp['id'] * 1;
58 46
             } else {

+ 4
- 0
lib/Strings.lib.php View File

@@ -21,6 +21,10 @@ class Strings {
21 21
 
22 22
         $this->load("en");
23 23
 
24
+        if ($language == "en") {
25
+            return;
26
+        }
27
+
24 28
         if (file_exists(__DIR__ . "/../langs/$language/")) {
25 29
             $this->language = $language;
26 30
             $this->load($language);

+ 16
- 147
lib/User.lib.php View File

@@ -17,22 +17,7 @@ class User {
17 17
 
18 18
     public function __construct(int $uid, string $username = "") {
19 19
         // Check if user exists
20
-        $client = new GuzzleHttp\Client();
21
-
22
-        $response = $client
23
-                ->request('POST', PORTAL_API, [
24
-            'form_params' => [
25
-                'key' => PORTAL_KEY,
26
-                'action' => "userexists",
27
-                'uid' => $uid
28
-            ]
29
-        ]);
30
-
31
-        if ($response->getStatusCode() > 299) {
32
-            sendError("Login server error: " . $response->getBody());
33
-        }
34
-
35
-        $resp = json_decode($response->getBody(), TRUE);
20
+        $resp = AccountHubApi::get("userexists", ["uid" => $uid]);
36 21
         if ($resp['status'] == "OK" && $resp['exists'] === true) {
37 22
             $this->exists = true;
38 23
         } else {
@@ -43,22 +28,7 @@ class User {
43 28
 
44 29
         if ($this->exists) {
45 30
             // Get user info
46
-            $client = new GuzzleHttp\Client();
47
-
48
-            $response = $client
49
-                    ->request('POST', PORTAL_API, [
50
-                'form_params' => [
51
-                    'key' => PORTAL_KEY,
52
-                    'action' => "userinfo",
53
-                    'uid' => $uid
54
-                ]
55
-            ]);
56
-
57
-            if ($response->getStatusCode() > 299) {
58
-                sendError("Login server error: " . $response->getBody());
59
-            }
60
-
61
-            $resp = json_decode($response->getBody(), TRUE);
31
+            $resp = AccountHubApi::get("userinfo", ["uid" => $uid]);
62 32
             if ($resp['status'] == "OK") {
63 33
                 $this->uid = $resp['data']['uid'] * 1;
64 34
                 $this->username = $resp['data']['username'];
@@ -71,22 +41,7 @@ class User {
71 41
     }
72 42
 
73 43
     public static function byUsername(string $username): User {
74
-        $client = new GuzzleHttp\Client();
75
-
76
-        $response = $client
77
-                ->request('POST', PORTAL_API, [
78
-            'form_params' => [
79
-                'key' => PORTAL_KEY,
80
-                'username' => $username,
81
-                'action' => "userinfo"
82
-            ]
83
-        ]);
84
-
85
-        if ($response->getStatusCode() > 299) {
86
-            sendError("Login server error: " . $response->getBody());
87
-        }
88
-
89
-        $resp = json_decode($response->getBody(), TRUE);
44
+        $resp = AccountHubApi::get("userinfo", ["username" => $username]);
90 45
         if (!isset($resp['status'])) {
91 46
             sendError("Login server error: " . $resp);
92 47
         }
@@ -105,22 +60,8 @@ class User {
105 60
         if (!$this->exists) {
106 61
             return false;
107 62
         }
108
-        $client = new GuzzleHttp\Client();
109
-
110
-        $response = $client
111
-                ->request('POST', PORTAL_API, [
112
-            'form_params' => [
113
-                'key' => PORTAL_KEY,
114
-                'action' => "hastotp",
115
-                'username' => $this->username
116
-            ]
117
-        ]);
118
-
119
-        if ($response->getStatusCode() > 299) {
120
-            sendError("Login server error: " . $response->getBody());
121
-        }
122 63
 
123
-        $resp = json_decode($response->getBody(), TRUE);
64
+        $resp = AccountHubApi::get("hastotp", ['username' => $this->username]);
124 65
         if ($resp['status'] == "OK") {
125 66
             return $resp['otp'] == true;
126 67
         } else {
@@ -147,26 +88,11 @@ class User {
147 88
     /**
148 89
      * Check the given plaintext password against the stored hash.
149 90
      * @param string $password
91
+     * @param bool $apppass Set to true to enforce app passwords when 2fa is on.
150 92
      * @return bool
151 93
      */
152
-    function checkPassword(string $password): bool {
153
-        $client = new GuzzleHttp\Client();
154
-
155
-        $response = $client
156
-                ->request('POST', PORTAL_API, [
157
-            'form_params' => [
158
-                'key' => PORTAL_KEY,
159
-                'action' => "auth",
160
-                'username' => $this->username,
161
-                'password' => $password
162
-            ]
163
-        ]);
164
-
165
-        if ($response->getStatusCode() > 299) {
166
-            sendError("Login server error: " . $response->getBody());
167
-        }
168
-
169
-        $resp = json_decode($response->getBody(), TRUE);
94
+    function checkPassword(string $password, bool $apppass = false): bool {
95
+        $resp = AccountHubApi::get("auth", ['username' => $this->username, 'password' => $password, 'apppass' => ($apppass ? "1" : "0")]);
170 96
         if ($resp['status'] == "OK") {
171 97
             return true;
172 98
         } else {
@@ -174,27 +100,13 @@ class User {
174 100
         }
175 101
     }
176 102
 
103
+
177 104
     function check2fa(string $code): bool {
178 105
         if (!$this->has2fa) {
179 106
             return true;
180 107
         }
181
-        $client = new GuzzleHttp\Client();
182 108
 
183
-        $response = $client
184
-                ->request('POST', PORTAL_API, [
185
-            'form_params' => [
186
-                'key' => PORTAL_KEY,
187
-                'action' => "verifytotp",
188
-                'username' => $this->username,
189
-                'code' => $code
190
-            ]
191
-        ]);
192
-
193
-        if ($response->getStatusCode() > 299) {
194
-            sendError("Login server error: " . $response->getBody());
195
-        }
196
-
197
-        $resp = json_decode($response->getBody(), TRUE);
109
+        $resp = AccountHubApi::get("verifytotp", ['username' => $this->username, 'code' => $code]);
198 110
         if ($resp['status'] == "OK") {
199 111
             return $resp['valid'];
200 112
         } else {
@@ -209,23 +121,7 @@ class User {
209 121
      * @return boolean TRUE if the user has the permission (or admin access), else FALSE
210 122
      */
211 123
     function hasPermission(string $code): bool {
212
-        $client = new GuzzleHttp\Client();
213
-
214
-        $response = $client
215
-                ->request('POST', PORTAL_API, [
216
-            'form_params' => [
217
-                'key' => PORTAL_KEY,
218
-                'action' => "permission",
219
-                'username' => $this->username,
220
-                'code' => $code
221
-            ]
222
-        ]);
223
-
224
-        if ($response->getStatusCode() > 299) {
225
-            sendError("Login server error: " . $response->getBody());
226
-        }
227
-
228
-        $resp = json_decode($response->getBody(), TRUE);
124
+        $resp = AccountHubApi::get("permission", ['username' => $this->username, 'code' => $code]);
229 125
         if ($resp['status'] == "OK") {
230 126
             return $resp['has_permission'];
231 127
         } else {
@@ -238,23 +134,7 @@ class User {
238 134
      * @return \AccountStatus
239 135
      */
240 136
     function getStatus(): AccountStatus {
241
-
242
-        $client = new GuzzleHttp\Client();
243
-
244
-        $response = $client
245
-                ->request('POST', PORTAL_API, [
246
-            'form_params' => [
247
-                'key' => PORTAL_KEY,
248
-                'action' => "acctstatus",
249
-                'username' => $this->username
250
-            ]
251
-        ]);
252
-
253
-        if ($response->getStatusCode() > 299) {
254
-            sendError("Login server error: " . $response->getBody());
255
-        }
256
-
257
-        $resp = json_decode($response->getBody(), TRUE);
137
+        $resp = AccountHubApi::get("acctstatus", ['username' => $this->username]);
258 138
         if ($resp['status'] == "OK") {
259 139
             return AccountStatus::fromString($resp['account']);
260 140
         } else {
@@ -262,24 +142,13 @@ class User {
262 142
         }
263 143
     }
264 144
 
265
-    function sendAlertEmail(string $appname = SITE_TITLE) {
266
-        $client = new GuzzleHttp\Client();
267
-
268
-        $response = $client
269
-                ->request('POST', PORTAL_API, [
270
-            'form_params' => [
271
-                'key' => PORTAL_KEY,
272
-                'action' => "alertemail",
273
-                'username' => $this->username,
274
-                'appname' => SITE_TITLE
275
-            ]
276
-        ]);
277
-
278
-        if ($response->getStatusCode() > 299) {
279
-            return "An unknown error occurred.";
145
+    function sendAlertEmail(string $appname = null) {
146
+        global $SETTINGS;
147
+        if (is_null($appname)) {
148
+            $appname = $SETTINGS['site_title'];
280 149
         }
150
+        $resp = AccountHubApi::get("alertemail", ['username' => $this->username, 'appname' => $SETTINGS['site_title']]);
281 151
 
282
-        $resp = json_decode($response->getBody(), TRUE);
283 152
         if ($resp['status'] == "OK") {
284 153
             return true;
285 154
         } else {

+ 4
- 4
lib/reports.php View File

@@ -38,7 +38,7 @@ if (LOADED) {
38 38
 }
39 39
 
40 40
 function getCashFlowReport($register = null, $start = null, $end = null) {
41
-    global $database, $Strings;
41
+    global $database, $Strings, $SETTINGS;
42 42
     $where = [];
43 43
 
44 44
     if (!is_null($register) && $database->has('registers', ['registerid' => $register])) {
@@ -85,11 +85,11 @@ function getCashFlowReport($register = null, $start = null, $end = null) {
85 85
     foreach ($cash as $c) {
86 86
         $registers[$c['registerid']]['name'] = $c['registername'];
87 87
         $registers[$c['registerid']]['id'] = $c['registerid'];
88
-        $registers[$c['registerid']]['open'] = date(DATETIME_FORMAT, strtotime($c['open']));
88
+        $registers[$c['registerid']]['open'] = date($SETTINGS['datetime_format'], strtotime($c['open']));
89 89
         if (is_null($c['close'])) {
90
-            $registers[$c['registerid']]['close'] = date(DATETIME_FORMAT);
90
+            $registers[$c['registerid']]['close'] = date($SETTINGS['datetime_format']);
91 91
         } else {
92
-            $registers[$c['registerid']]['close'] = date(DATETIME_FORMAT, strtotime($c['close']));
92
+            $registers[$c['registerid']]['close'] = date($SETTINGS['datetime_format'], strtotime($c['close']));
93 93
         }
94 94
         if (!isset($registers[$c['registerid']][$c['typename']])) {
95 95
             $registers[$c['registerid']][$c['typename']] = 0.0;

+ 17
- 45
mobile/index.php View File

@@ -8,10 +8,6 @@
8 8
  * Mobile app API
9 9
  */
10 10
 
11
-// The name of the permission needed to log in.
12
-// Set to null if you don't need it.
13
-$access_permission = null;
14
-
15 11
 require __DIR__ . "/../required.php";
16 12
 
17 13
 header('Content-Type: application/json');
@@ -23,21 +19,7 @@ if ($VARS['action'] == "ping") {
23 19
 }
24 20
 
25 21
 function mobile_enabled() {
26
-    $client = new GuzzleHttp\Client();
27
-
28
-    $response = $client
29
-            ->request('POST', PORTAL_API, [
30
-        'form_params' => [
31
-            'key' => PORTAL_KEY,
32
-            'action' => "mobileenabled"
33
-        ]
34
-    ]);
35
-
36
-    if ($response->getStatusCode() > 299) {
37
-        return false;
38
-    }
39
-
40
-    $resp = json_decode($response->getBody(), TRUE);
22
+    $resp = AccountHubApi::get("mobileenabled");
41 23
     if ($resp['status'] == "OK" && $resp['mobile'] === TRUE) {
42 24
         return true;
43 25
     } else {
@@ -46,26 +28,15 @@ function mobile_enabled() {
46 28
 }
47 29
 
48 30
 function mobile_valid($username, $code) {
49
-    $client = new GuzzleHttp\Client();
50
-
51
-    $response = $client
52
-            ->request('POST', PORTAL_API, [
53
-        'form_params' => [
54
-            'key' => PORTAL_KEY,
55
-            "code" => $code,
56
-            "username" => $username,
57
-            'action' => "mobilevalid"
58
-        ]
59
-    ]);
31
+    try {
32
+        $resp = AccountHubApi::get("mobilevalid", ["code" => $code, "username" => $username], true);
60 33
 
61
-    if ($response->getStatusCode() > 299) {
62
-        return false;
63
-    }
64
-
65
-    $resp = json_decode($response->getBody(), TRUE);
66
-    if ($resp['status'] == "OK" && $resp['valid'] === TRUE) {
67
-        return true;
68
-    } else {
34
+        if ($resp['status'] == "OK" && $resp['valid'] === TRUE) {
35
+            return true;
36
+        } else {
37
+            return false;
38
+        }
39
+    } catch (Exception $ex) {
69 40
         return false;
70 41
     }
71 42
 }
@@ -75,7 +46,7 @@ if (mobile_enabled() !== TRUE) {
75 46
 }
76 47
 
77 48
 // Make sure we have a username and access key
78
-if (is_empty($VARS['username']) || is_empty($VARS['key'])) {
49
+if (empty($VARS['username']) || empty($VARS['key'])) {
79 50
     http_response_code(401);
80 51
     die(json_encode(["status" => "ERROR", "msg" => "Missing username and/or access key."]));
81 52
 }
@@ -95,13 +66,14 @@ switch ($VARS['action']) {
95 66
         if ($user->exists()) {
96 67
             if ($user->getStatus()->getString() == "NORMAL") {
97 68
                 if ($user->checkPassword($VARS['password'])) {
98
-                    if (is_null($access_permission) || $user->hasPermission($access_permission)) {
99
-                        Session::start($user);
100
-                        $_SESSION['mobile'] = true;
101
-                        exit(json_encode(["status" => "OK"]));
102
-                    } else {
103
-                        exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("no admin permission", false)]));
69
+                    foreach ($SETTINGS['permissions'] as $perm) {
70
+                        if (!$user->hasPermission($perm)) {
71
+                            exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("no permission", false)]));
72
+                        }
104 73
                     }
74
+                    Session::start($user);
75
+                    $_SESSION['mobile'] = true;
76
+                    exit(json_encode(["status" => "OK"]));
105 77
                 }
106 78
             }
107 79
         }

+ 8
- 0
pages.php View File

@@ -138,5 +138,13 @@ define("PAGES", [
138 138
     ],
139 139
     "404" => [
140 140
         "title" => "404 error"
141
+    ],
142
+    "form" => [
143
+        "title" => "Form",
144
+        "navbar" => true,
145
+        "icon" => "fas fa-file-alt",
146
+        "scripts" => [
147
+            "static/js/form.js"
148
+        ]
141 149
     ]
142 150
 ]);

+ 1
- 1
pages/certificates.php View File

@@ -39,7 +39,7 @@ $cards = $database->select('certificates', ['certid (id)', 'certcode (code)', 'a
39 39
                 <td><?php echo $c['code']; ?></td>
40 40
                 <td>$<?php echo number_format($c['amount'], 2); ?></td>
41 41
                 <td>$<?php echo number_format($c['start'], 2); ?></td>
42
-                <td><?php echo date(DATETIME_FORMAT, strtotime($c['issued'])); ?></td>
42
+                <td><?php echo date($SETTINGS['datetime_format'], strtotime($c['issued'])); ?></td>
43 43
             </tr>
44 44
             <?php
45 45
         }

+ 1
- 1
pages/editcertificate.php View File

@@ -16,7 +16,7 @@ $carddata = [
16 16
 
17 17
 $editing = false;
18 18
 
19
-if (!empty($VARS['id']) && !is_empty($VARS['id'])) {
19
+if (!empty($VARS['id'])) {
20 20
     if ($database->has('certificates', ['certid' => $VARS['id']])) {
21 21
         $editing = true;
22 22
         $carddata = $database->get(

+ 1
- 1
pages/editcustomer.php View File

@@ -18,7 +18,7 @@ $custdata = [
18 18
 
19 19
 $editing = false;
20 20
 
21
-if (!empty($VARS['id']) && !is_empty($VARS['id'])) {
21
+if (!empty($VARS['id'])) {
22 22
     if ($database->has('customers', ['customerid' => $VARS['id']])) {
23 23
         $editing = true;
24 24
         $custdata = $database->get(

+ 2
- 2
pages/editregister.php View File

@@ -70,9 +70,9 @@ if ($editing) {
70 70
                         for ($i = count($cash) - 1; $i >= 0; $i--) {
71 71
                             $c = $cash[$i];
72 72
                             echo "<option value=\"$c[cashid]\">"
73
-                            . date(DATETIME_FORMAT, strtotime($c['open']))
73
+                            . date($SETTINGS['datetime_format'], strtotime($c['open']))
74 74
                             . ' - '
75
-                            . date(DATETIME_FORMAT, strtotime($c['close']))
75
+                            . date($SETTINGS['datetime_format'], strtotime($c['close']))
76 76
                             . "</option>";
77 77
                         }
78 78
                         ?>

+ 1
- 1
pages/pos.php View File

@@ -18,7 +18,7 @@ if (isset($_GET['switch']) || !isset($_SESSION['register']) || !$registeropen) {
18 18
     require_once __DIR__ . "/../lib/chooseregister.php";
19 19
 } else {
20 20
     $register = $database->get('registers', ['registerid (id)', 'registername (name)'], ['registerid' => $_SESSION['register']]);
21
-    $showgridbydefault = $binstack->count('items', ['AND' => ['price[!]' => null, 'price[!]' => 0]]) <= GRID_BY_DEFAULT_MAX_ITEMS;
21
+    $showgridbydefault = $binstack->count('items', ['AND' => ['price[!]' => null, 'price[!]' => 0]]) <= $SETTINGS['grid_default_max_items'];
22 22
     $items = [];
23 23
     $payments = [];
24 24
     $editing = false;

+ 2
- 2
pages/registers.php View File

@@ -65,8 +65,8 @@ $registers = $database->select('registers', ['registerid (id)', 'registername (n
65 65
                         $balance += $row;
66 66
                     }
67 67
                 }
68
-                $open = date(DATETIME_FORMAT, strtotime($cash['open']));
69
-                $close = is_null($cash['close']) ? $Strings->get("still open", false) : date(DATETIME_FORMAT, strtotime($cash['close']));
68
+                $open = date($SETTINGS['datetime_format'], strtotime($cash['open']));
69
+                $close = is_null($cash['close']) ? $Strings->get("still open", false) : date($SETTINGS['datetime_format'], strtotime($cash['close']));
70 70
             }
71 71
             ?>
72 72
             <tr>

+ 2
- 1
public/lib/item.php View File

@@ -108,7 +108,8 @@ class ItemImage {
108 108
     }
109 109
 
110 110
     function getAbsoluteUrl(): string {
111
-        return BINSTACK_URL_IMAGEPHP . "?i=" . $this->url;
111
+        global $SETTINGS;
112
+        return $SETTINGS['binstack_image.php'] . "?i=" . $this->url;
112 113
     }
113 114
 
114 115
     function isPrimary(): bool {

+ 1
- 1
public/parts/account.php View File

@@ -34,7 +34,7 @@ if ($loggedin !== true || is_null($account)) {
34 34
                     }
35 35
                     ?>
36 36
                     <div class="list-group-item">
37
-                        Date: <?php echo date(DATETIME_FORMAT, strtotime($o['txdate'])); ?><br />
37
+                        Date: <?php echo date($SETTINGS['datetime_format'], strtotime($o['txdate'])); ?><br />
38 38
                         Type: <?php
39 39
                         switch ($o['type']) {
40 40
                             case 1:

+ 17
- 26
public/required.php View File

@@ -40,8 +40,8 @@ header("Content-Security-Policy: "
40 40
         . "frame-src 'self'; "
41 41
         . "font-src 'self'; "
42 42
         . "connect-src *; "
43
-        . "style-src 'self' 'nonce-$SECURE_NONCE' $captcha_server; "
44
-        . "script-src 'self' 'nonce-$SECURE_NONCE' $captcha_server");
43
+        . "style-src 'self' 'nonce-$SECURE_NONCE'; "
44
+        . "script-src 'self' 'nonce-$SECURE_NONCE'");
45 45
 
46 46
 //
47 47
 // Composer
@@ -69,7 +69,7 @@ function sendError($error) {
69 69
             . "<p>" . htmlspecialchars($error) . "</p>");
70 70
 }
71 71
 
72
-date_default_timezone_set(TIMEZONE);
72
+date_default_timezone_set($SETTINGS['timezone']);
73 73
 
74 74
 // Database settings
75 75
 // Also inits database and stuff
@@ -79,20 +79,20 @@ $database;
79 79
 $binstack;
80 80
 try {
81 81
     $database = new Medoo([
82
-        'database_type' => DB_TYPE,
83
-        'database_name' => DB_NAME,
84
-        'server' => DB_SERVER,
85
-        'username' => DB_USER,
86
-        'password' => DB_PASS,
87
-        'charset' => DB_CHARSET
82
+        'database_type' => $SETTINGS['database']['type'],
83
+        'database_name' => $SETTINGS['database']['name'],
84
+        'server' => $SETTINGS['database']['server'],
85
+        'username' => $SETTINGS['database']['user'],
86
+        'password' => $SETTINGS['database']['password'],
87
+        'charset' => $SETTINGS['database']['charset']
88 88
     ]);
89 89
     $binstack = new Medoo([
90
-        'database_type' => BINSTACK_DB_TYPE,
91
-        'database_name' => BINSTACK_DB_NAME,
92
-        'server' => BINSTACK_DB_SERVER,
93
-        'username' => BINSTACK_DB_USER,
94
-        'password' => BINSTACK_DB_PASS,
95
-        'charset' => BINSTACK_DB_CHARSET
90
+        'database_type' => $SETTINGS['binstack_database']['type'],
91
+        'database_name' => $SETTINGS['binstack_database']['name'],
92
+        'server' => $SETTINGS['binstack_database']['server'],
93
+        'username' => $SETTINGS['binstack_database']['user'],
94
+        'password' => $SETTINGS['binstack_database']['password'],
95
+        'charset' => $SETTINGS['binstack_database']['charset']
96 96
     ]);
97 97
 } catch (Exception $ex) {
98 98
     //header('HTTP/1.1 500 Internal Server Error');
@@ -100,7 +100,7 @@ try {
100 100
 }
101 101
 
102 102
 
103
-if (!DEBUG) {
103
+if (!$SETTINGS['debug']) {
104 104
     error_reporting(0);
105 105
 } else {
106 106
     error_reporting(E_ALL);
@@ -127,13 +127,4 @@ if (!empty($_SESSION['shop_account'])) {
127 127
     } else {
128 128
         $account = null;
129 129
     }
130
-}
131
-
132
-/**
133
- * Checks if a string or whatever is empty.
134
- * @param $str The thingy to check
135
- * @return boolean True if it's empty or whatever.
136
- */
137
-function is_empty($str) {
138
-    return (is_null($str) || !isset($str) || $str == '');
139
-}
130
+}

+ 37
- 63
required.php View File

@@ -32,7 +32,6 @@ session_start(); // stick some cookies in it
32 32
 // renew session cookie
33 33
 setcookie(session_name(), session_id(), time() + $session_length, "/", false, false);
34 34
 
35
-$captcha_server = (CAPTCHA_ENABLED === true ? preg_replace("/http(s)?:\/\//", "", CAPTCHA_SERVER) : "");
36 35
 if ($_SESSION['mobile'] === TRUE) {
37 36
     header("Content-Security-Policy: "
38 37
             . "default-src 'self';"
@@ -42,8 +41,8 @@ if ($_SESSION['mobile'] === TRUE) {
42 41
             . "frame-src 'self'; "
43 42
             . "font-src 'self'; "
44 43
             . "connect-src *; "
45
-            . "style-src 'self' 'unsafe-inline' $captcha_server; "
46
-            . "script-src 'self' 'unsafe-inline' $captcha_server");
44
+            . "style-src 'self' 'unsafe-inline'; "
45
+            . "script-src 'self' 'unsafe-inline'");
47 46
 } else {
48 47
     header("Content-Security-Policy: "
49 48
             . "default-src 'self';"
@@ -53,8 +52,8 @@ if ($_SESSION['mobile'] === TRUE) {
53 52
             . "frame-src 'self'; "
54 53
             . "font-src 'self'; "
55 54
             . "connect-src *; "
56
-            . "style-src 'self' 'nonce-$SECURE_NONCE' $captcha_server; "
57
-            . "script-src 'self' 'nonce-$SECURE_NONCE' $captcha_server");
55
+            . "style-src 'self' 'nonce-$SECURE_NONCE'; "
56
+            . "script-src 'self' 'nonce-$SECURE_NONCE'");
58 57
 }
59 58
 
60 59
 //
@@ -69,7 +68,7 @@ foreach ($libs as $lib) {
69 68
     require_once $lib;
70 69
 }
71 70
 
72
-$Strings = new Strings(LANGUAGE);
71
+$Strings = new Strings($SETTINGS['language']);
73 72
 
74 73
 /**
75 74
  * Kill off the running process and spit out an error message
@@ -93,7 +92,7 @@ function sendError($error) {
93 92
             . "<p>" . htmlspecialchars($error) . "</p>");
94 93
 }
95 94
 
96
-date_default_timezone_set(TIMEZONE);
95
+date_default_timezone_set($SETTINGS['timezone']);
97 96
 
98 97
 // Database settings
99 98
 // Also inits database and stuff
@@ -103,20 +102,20 @@ $database;
103 102
 $binstack;
104 103
 try {
105 104
     $database = new Medoo([
106
-        'database_type' => DB_TYPE,
107
-        'database_name' => DB_NAME,
108
-        'server' => DB_SERVER,
109
-        'username' => DB_USER,
110
-        'password' => DB_PASS,
111
-        'charset' => DB_CHARSET
105
+        'database_type' => $SETTINGS['database']['type'],
106
+        'database_name' => $SETTINGS['database']['name'],
107
+        'server' => $SETTINGS['database']['server'],
108
+        'username' => $SETTINGS['database']['user'],
109
+        'password' => $SETTINGS['database']['password'],
110
+        'charset' => $SETTINGS['database']['charset']
112 111
     ]);
113 112
     $binstack = new Medoo([
114
-        'database_type' => BINSTACK_DB_TYPE,
115
-        'database_name' => BINSTACK_DB_NAME,
116
-        'server' => BINSTACK_DB_SERVER,
117
-        'username' => BINSTACK_DB_USER,
118
-        'password' => BINSTACK_DB_PASS,
119
-        'charset' => BINSTACK_DB_CHARSET
113
+        'database_type' => $SETTINGS['binstack_database']['type'],
114
+        'database_name' => $SETTINGS['binstack_database']['name'],
115
+        'server' => $SETTINGS['binstack_database']['server'],
116
+        'username' => $SETTINGS['binstack_database']['user'],
117
+        'password' => $SETTINGS['binstack_database']['password'],
118
+        'charset' => $SETTINGS['binstack_database']['charset']
120 119
     ]);
121 120
 } catch (Exception $ex) {
122 121
     //header('HTTP/1.1 500 Internal Server Error');
@@ -124,7 +123,7 @@ try {
124 123
 }
125 124
 
126 125
 
127
-if (!DEBUG) {
126
+if (!$SETTINGS['debug']) {
128 127
     error_reporting(0);
129 128
 } else {
130 129
     error_reporting(E_ALL);
@@ -141,20 +140,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
141 140
     define("GET", true);
142 141
 }
143 142
 
144
-/**
145
- * Checks if a string or whatever is empty.
146
- * @param $str The thingy to check
147
- * @return boolean True if it's empty or whatever.
148
- */
149
-function is_empty($str) {
150
-    return (is_null($str) || !isset($str) || $str == '');
151
-}
152
-
153
-
154 143
 function dieifnotloggedin() {
144
+    global $SETTINGS;
155 145
     if ($_SESSION['loggedin'] != true) {
156 146
         sendError("Session expired.  Please log out and log in again.");
157 147
     }
148
+    $user = new User($_SESSION['uid']);
149
+    foreach ($SETTINGS['permissions'] as $perm) {
150
+        if (!$user->hasPermission($perm)) {
151
+            session_destroy();
152
+            die("You don't have permission to be here.");
153
+        }
154
+    }
158 155
 }
159 156
 
160 157
 /**
@@ -174,41 +171,18 @@ function checkDBError($specials = []) {
174 171
     }
175 172
 }
176 173
 
177
-/*
178
- * http://stackoverflow.com/a/20075147
179
- */
180
-if (!function_exists('base_url')) {
181
-
182
-    function base_url($atRoot = FALSE, $atCore = FALSE, $parse = FALSE) {
183
-        if (isset($_SERVER['HTTP_HOST'])) {
184
-            $http = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off' ? 'https' : 'http';
185
-            $hostname = $_SERVER['HTTP_HOST'];
186
-            $dir = str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']);
187
-
188
-            $core = preg_split('@/@', str_replace($_SERVER['DOCUMENT_ROOT'], '', realpath(dirname(__FILE__))), NULL, PREG_SPLIT_NO_EMPTY);
189
-            $core = $core[0];
190
-
191
-            $tmplt = $atRoot ? ($atCore ? "%s://%s/%s/" : "%s://%s/") : ($atCore ? "%s://%s/%s/" : "%s://%s%s");
192
-            $end = $atRoot ? ($atCore ? $core : $hostname) : ($atCore ? $core : $dir);
193
-            $base_url = sprintf($tmplt, $http, $hostname, $end);
194
-        } else
195
-            $base_url = 'http://localhost/';
196
-
197
-        if ($parse) {
198
-            $base_url = parse_url($base_url);
199
-            if (isset($base_url['path']))
200
-                if ($base_url['path'] == '/')
201
-                    $base_url['path'] = '';
202
-        }
203
-
204
-        return $base_url;
205
-    }
206
-
207
-}
208
-
209 174
 function redirectIfNotLoggedIn() {
175
+    global $SETTINGS;
210 176
     if ($_SESSION['loggedin'] !== TRUE) {
211
-        header('Location: ' . URL . '/index.php');
177
+        header('Location: ' . $SETTINGS['url'] . '/index.php');
212 178
         die();
213 179
     }
180
+    $user = new User($_SESSION['uid']);
181
+    foreach ($SETTINGS['permissions'] as $perm) {
182
+        if (!$user->hasPermission($perm)) {
183
+            session_destroy();
184
+            header('Location: ./index.php');
185
+            die("You don't have permission to be here.");
186
+        }
187
+    }
214 188
 }

+ 71
- 60
settings.template.php View File

@@ -1,65 +1,76 @@
1 1
 <?php
2 2
 
3
-/* This Source Code Form is subject to the terms of the Mozilla Public
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
4 5
  * License, v. 2.0. If a copy of the MPL was not distributed with this
5
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
6 8
 
7
-// Whether to show debugging data in output.
8
-// DO NOT SET TO TRUE IN PRODUCTION!!!
9
-define("DEBUG", false);
9
+// Settings for the app.
10
+// Copy to settings.php and customize.
10 11
 
11
-// Database connection settings
12
-// See http://medoo.in/api/new for info
13
-define("DB_TYPE", "mysql");
14
-define("DB_NAME", "nickelbox");
15
-define("DB_SERVER", "localhost");
16
-define("DB_USER", "nickelbox");
17
-define("DB_PASS", "");
18
-define("DB_CHARSET", "utf8");
19
-
20
-// BinStack database connection settings
21
-define("BINSTACK_DB_TYPE", "mysql");
22
-define("BINSTACK_DB_NAME", "inventory");
23
-define("BINSTACK_DB_SERVER", "localhost");
24
-define("BINSTACK_DB_USER", "inventory");
25
-define("BINSTACK_DB_PASS", "");
26
-define("BINSTACK_DB_CHARSET", "utf8");
27
-
28
-// Absolute path to image.php in the BinStack installation folder
29
-// Required for item images to load
30
-define("BINSTACK_URL_IMAGEPHP", "/binstack/image.php");
31
-
32
-// Name of the app.
33
-define("SITE_TITLE", "NickelBox");
34
-
35
-// If there are this many or fewer items,
36
-// load the POS grid view automatically
37
-define("GRID_BY_DEFAULT_MAX_ITEMS", 20);
38
-
39
-// URL of the AccountHub API endpoint
40
-define("PORTAL_API", "http://localhost/accounthub/api.php");
41
-// URL of the AccountHub home page
42
-define("PORTAL_URL", "http://localhost/accounthub/home.php");
43
-// AccountHub API Key
44
-define("PORTAL_KEY", "123");
45
-
46
-// For supported values, see http://php.net/manual/en/timezones.php
47
-define("TIMEZONE", "America/Denver");
48
-
49
-define("DATETIME_FORMAT", "M j Y g:i A"); // 12 hour time
50
-#define("DATETIME_FORMAT", "M j Y G:i"); // 24 hour time
51
-
52
-// Base URL for site links.
53
-define('URL', '.');
54
-
55
-// Use Captcheck on login screen
56
-// https://captcheck.netsyms.com
57
-define("CAPTCHA_ENABLED", FALSE);
58
-define('CAPTCHA_SERVER', 'https://captcheck.netsyms.com');
59
-
60
-// See lang folder for language options
61
-define('LANGUAGE', "en_us");
62
-
63
-
64
-define("FOOTER_TEXT", "");
65
-define("COPYRIGHT_NAME", "Netsyms Technologies");
12
+$SETTINGS = [
13
+    // Whether to output debugging info like PHP notices, warnings,
14
+    // and stacktraces.
15
+    // Turning this on in production is a security risk and can sometimes break
16
+    // things, such as JSON output where extra content is not expected.
17
+    "debug" => false,
18
+    // Database connection settings
19
+    // See http://medoo.in/api/new for info
20
+    "database" => [
21
+        "type" => "mysql",
22
+        "name" => "nickelbox",
23
+        "server" => "localhost",
24
+        "user" => "",
25
+        "password" => "",
26
+        "charset" => "utf8"
27
+    ],
28
+    // BinStack database
29
+    // Needed for integration
30
+    "binstack_database" => [
31
+        "type" => "mysql",
32
+        "name" => "binstack",
33
+        "server" => "localhost",
34
+        "user" => "",
35
+        "password" => "",
36
+        "charset" => "utf8"
37
+    ],
38
+    // Name of the app.
39
+    "site_title" => "NickelBox",
40
+    // Settings for connecting to the AccountHub server.
41
+    "accounthub" => [
42
+        // URL for the API endpoint
43
+        "api" => "http://localhost/accounthub/api/",
44
+        // URL of the home page
45
+        "home" => "http://localhost/accounthub/home.php",
46
+        // API key
47
+        "key" => "123"
48
+    ],
49
+    // Absolute path to image.php in the BinStack installation folder
50
+    // Required for item images to load
51
+    "binstack_image.php" => "/binstack/image.php",
52
+    // If there are this many or fewer items,
53
+    // load the POS grid view automatically
54
+    "grid_default_max_items" => 20,
55
+    // PHP date() format string for date display
56
+    "datetime_format" => "M j Y g:i A",
57
+    // List of required user permissions to access this app.
58
+    "permissions" => [
59
+    ],
60
+    // List of permissions required for API access. Remove to use the value of
61
+    // "permissions" instead.
62
+    "api_permissions" => [
63
+    ],
64
+    // For supported values, see http://php.net/manual/en/timezones.php
65
+    "timezone" => "America/Denver",
66
+    // Language to use for localization. See langs folder to add a language.
67
+    "language" => "en",
68
+    // Shown in the footer of all the pages.
69
+    "footer_text" => "",
70
+    // Also shown in the footer, but with "Copyright <current_year>" in front.
71
+    "copyright" => "Netsyms Technologies",
72
+    // Base URL for building links relative to the location of the app.
73
+    // Only used when there's no good context for the path.
74
+    // The default is almost definitely fine.
75
+    "url" => "."
76
+];

+ 6
- 6
static/css/bootstrap.min.css
File diff suppressed because it is too large
View File


+ 0
- 15
static/css/index.css View File

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

+ 1
- 5
static/css/svg-with-js.min.css View File

@@ -1,5 +1 @@
1
-/*!
2
- * Font Awesome Free 5.3.1 by @fontawesome - https://fontawesome.com
3
- * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4
- */
5
-.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{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;position:relative;width:2em}.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:1em}.svg-inline--fa.fa-stack-2x{height:2em;width:2em}.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}
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}

+ 1
- 2
static/js/app.js View File

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

+ 3
- 3
static/js/bootstrap.bundle.min.js
File diff suppressed because it is too large
View File


+ 1
- 5
static/js/fontawesome-all.min.js
File diff suppressed because it is too large
View File


Loading…
Cancel
Save