Compare commits

...

49 Commits
v1.2 ... master

Author SHA1 Message Date
d72c192294 Merge pull request 'Add stock management (#14)' (#1) from stock_management into master
Business/BinStack#14
2020-03-13 00:04:33 -06:00
3d2cd94494 Fix some labels 2020-03-11 14:23:08 +02:00
826de5246b Fix item deletion 2020-03-11 10:33:07 +02:00
488988f7d4 Add stock movements report 2020-03-11 09:00:00 +02:00
841c03847e Add stock management mode setting 2020-03-10 10:48:35 +02:00
5c0bea68a9 Add stock table to EER diagram 2020-03-10 09:02:08 +02:00
a960d5143f Add stock transactions to item page 2020-03-09 13:18:36 +02:00
0e2c2c17a2 UI tweaks 2020-02-18 13:38:48 +02:00
8964202a1a Add basic stock management 2020-02-18 11:03:20 +02:00
44bde6a116 Fix database.mwb 2019-03-19 17:25:09 -06:00
d30b6fc27c Merge ../BusinessAppTemplate 2019-03-19 17:16:05 -06:00
53e158b553 Update FontAwesome 5.6.0 -> 5.7.2 2019-03-19 17:12:45 -06:00
474047ab34 Update Bootstrap 4.1.3 -> 4.3.1 2019-03-19 17:11:54 -06:00
c97e058786 API: Check for user permission 2019-03-01 23:43:31 -07:00
26a662c399 Add addTextInput and addSelect methods that are simpler than addInput 2019-03-01 23:37:36 -07:00
289aaeaa9f Minor text changes 2019-03-01 23:37:06 -07:00
3ca062d995 Enforce app passwords in API for users with two-factor enabled 2019-02-11 16:19:27 -07:00
7d30251cd6 Add CORS header to API 2019-01-07 22:18:02 -07:00
5e1ee303a8 Merge ../BusinessAppTemplate
# Conflicts:
#	pages/form.php
2019-01-04 17:58:49 -07:00
e1837a22fe Merge ../BusinessAppTemplate
# Conflicts:
#	README.md
#	api.php
#	index.php
#	langs/en/core.json
#	langs/en/titles.json
#	mobile/index.php
#	required.php
#	settings.template.php
2019-01-04 17:57:08 -07:00
7173a50c36 Add textarea to FormBuilder 2019-01-04 17:53:01 -07:00
e66280e07a FormBuilder: add d-flex to footer 2019-01-04 17:29:08 -07:00
3ed75822a1 Update license and readme 2019-01-03 00:13:41 -07:00
7531dc362d Whoops 2019-01-02 23:54:53 -07:00
b250908663 Add more permissions checks 2019-01-02 23:51:47 -07:00
892102528b Strip tags from aria-label 2018-12-31 14:22:14 -07:00
69c634ea99 Add checkbox to form builder 2018-12-31 14:14:00 -07:00
6ceeeaa087 Add support for regex matching on API vars 2018-12-27 14:44:10 -07:00
f1c36fdeb1 Add getRequestUser() function 2018-12-27 00:55:58 -07:00
d36b340692 Make API work with user/pass combo 2018-12-27 00:51:54 -07:00
d7ca7125ce Nicer access denied message 2018-12-27 00:15:27 -07:00
1729b842ba Add permission check during login 2018-12-26 16:32:43 -07:00
4d2b78bdba Remove unused strings 2018-12-26 16:28:32 -07:00
106e697fc3 Remove captcha-related code, since login is done by AccountHub now 2018-12-26 16:25:48 -07:00
e0802f582b Remove unneeded index.css 2018-12-26 16:22:09 -07:00
016c71d30d Fix index.php not redirecting to app.php when already logged in 2018-12-22 22:38:50 -07:00
ba1369d842 Add app icon to login flow 2018-12-22 21:26:57 -07:00
a559901ac0 Redirect to AccountHub for user login 2018-12-22 16:57:45 -07:00
3f32258ba0 Fix bug 2018-12-20 23:58:35 -07:00
129efd13c7 Add documentation comments to settings 2018-12-20 23:54:25 -07:00
c179ed7ebb Make settings.php an array, not a bunch of defines 2018-12-20 23:45:45 -07:00
f1a85f47fd Add comment 2018-12-20 23:25:34 -07:00
61d660be69 Add FormBuilder 2018-12-20 23:24:47 -07:00
5b7ab65946 Make better API system, use new AccountHub API 2018-12-14 21:16:31 -07:00
13b60de915 Remove is_empty() 2018-12-14 19:14:22 -07:00
32cd18933d Update FontAwesome from 5.3.1 to 5.6.0 2018-12-11 21:39:25 -07:00
4f1b81ff4b Deprecate is_empty() 2018-12-04 19:48:23 -07:00
cb3c8aaf2d Support undefined messages 2018-12-04 19:46:08 -07:00
ec44a6740f Fix "language key ... is defined more than once" warning 2018-11-28 22:50:29 -07:00
56 changed files with 1502 additions and 955 deletions

View File

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

View File

@ -21,11 +21,11 @@ if ($VARS['action'] !== "signout") {
*/
function returnToSender($msg, $arg = "") {
global $VARS;
if ($arg == "") {
header("Location: app.php?page=" . urlencode($VARS['source']) . "&msg=" . $msg);
} else {
header("Location: app.php?page=" . urlencode($VARS['source']) . "&msg=$msg&arg=$arg");
$header = "Location: app.php?page=" . urlencode($VARS['source']) . "&msg=$msg";
if ($arg != "") {
$header .= "&arg=$arg";
}
header($header);
die();
}
@ -36,7 +36,7 @@ if ($VARS['action'] != "signout" && !(new User($_SESSION['uid']))->hasPermission
switch ($VARS['action']) {
case "edititem":
$insert = true;
if (is_empty($VARS['itemid'])) {
if (empty($VARS['itemid'])) {
$insert = true;
} else {
if ($database->has('items', ['itemid' => $VARS['itemid']])) {
@ -45,42 +45,42 @@ switch ($VARS['action']) {
returnToSender("invalid_itemid");
}
}
if (is_empty($VARS['name'])) {
if (empty($VARS['name'])) {
returnToSender('missing_name');
}
if (!is_empty($VARS['catstr']) && is_empty($VARS['cat'])) {
if (!empty($VARS['catstr']) && empty($VARS['cat'])) {
if ($database->count("categories", ["catname" => $VARS['catstr']]) == 1) {
$VARS['cat'] = $database->get("categories", 'catid', ["catname" => $VARS['catstr']]);
} else {
returnToSender('use_the_drop_luke');
}
}
if (!is_empty($VARS['locstr']) && is_empty($VARS['loc'])) {
if (!empty($VARS['locstr']) && empty($VARS['loc'])) {
if ($database->count("locations", ["locname" => $VARS['locstr']]) == 1) {
$VARS['loc'] = $database->get("locations", 'locid', ["locname" => $VARS['locstr']]);
} else {
returnToSender('use_the_drop_luke');
}
}
if (is_empty($VARS['cat']) || is_empty($VARS['loc'])) {
if (empty($VARS['cat']) || empty($VARS['loc'])) {
returnToSender('invalid_parameters');
}
if (is_empty($VARS['qty'])) {
if (empty($VARS['qty'])) {
$VARS['qty'] = 1;
} else if (!is_numeric($VARS['qty'])) {
returnToSender('field_nan');
}
if (is_empty($VARS['want'])) {
if (empty($VARS['want'])) {
$VARS['want'] = 0;
} else if (!is_numeric($VARS['want'])) {
returnToSender('field_nan');
}
if (is_empty($VARS['cost'])) {
if (empty($VARS['cost'])) {
$VARS['cost'] = null;
} else if (!is_numeric($VARS['cost'])) {
returnToSender('field_nan');
}
if (is_empty($VARS['price'])) {
if (empty($VARS['price'])) {
$VARS['price'] = null;
} else if (!is_numeric($VARS['price'])) {
returnToSender('field_nan');
@ -128,7 +128,7 @@ switch ($VARS['action']) {
returnToSender("item_saved");
case "editcat":
$insert = true;
if (is_empty($VARS['catid'])) {
if (empty($VARS['catid'])) {
$insert = true;
} else {
if ($database->has('categories', ['catid' => $VARS['catid']])) {
@ -137,7 +137,7 @@ switch ($VARS['action']) {
returnToSender("invalid_catid");
}
}
if (is_empty($VARS['name'])) {
if (empty($VARS['name'])) {
returnToSender('invalid_parameters');
}
@ -154,7 +154,7 @@ switch ($VARS['action']) {
returnToSender("category_saved");
case "editloc":
$insert = true;
if (is_empty($VARS['locid'])) {
if (empty($VARS['locid'])) {
$insert = true;
} else {
if ($database->has('locations', ['locid' => $VARS['locid']])) {
@ -163,7 +163,7 @@ switch ($VARS['action']) {
returnToSender("invalid_locid");
}
}
if (is_empty($VARS['name'])) {
if (empty($VARS['name'])) {
returnToSender('invalid_parameters');
}
@ -217,9 +217,9 @@ switch ($VARS['action']) {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
->request('POST', $SETTINGS['accounthub']['api'], [
'form_params' => [
'key' => PORTAL_KEY,
'key' => $SETTINGS['accounthub']['key'],
'action' => "usersearch",
'search' => $VARS['q']
]
@ -237,7 +237,7 @@ switch ($VARS['action']) {
}
break;
case "imageupload":
$destpath = FILE_UPLOAD_PATH;
$destpath = $SETTINGS['file_upload_path'];
if (!is_writable($destpath)) {
returnToSender("unwritable_folder", "&id=$VARS[itemid]");
}
@ -274,7 +274,7 @@ switch ($VARS['action']) {
default:
$err = "could not be uploaded.";
}
$errors[] = htmlspecialchars($f['name']) . " $err";
$errors[] = htmlentities($f['name']) . " $err";
continue;
}
@ -296,7 +296,7 @@ switch ($VARS['action']) {
}
if (!$imagevalid) {
$errors[] = htmlspecialchars($f['name']) . " is not a supported image type (JPEG, GIF, PNG, WEBP).";
$errors[] = htmlentities($f['name']) . " is not a supported image type (JPEG, GIF, PNG, WEBP).";
continue;
}
@ -319,7 +319,7 @@ switch ($VARS['action']) {
}
$database->insert('images', ['itemid' => $VARS['itemid'], 'imagename' => $filename, 'primary' => $primary]);
} else {
$errors[] = htmlspecialchars($f['name']) . " could not be uploaded.";
$errors[] = htmlentities($f['name']) . " could not be uploaded.";
}
}
@ -350,7 +350,7 @@ switch ($VARS['action']) {
$imagename = $database->get('images', 'imagename', ['imageid' => $VARS['imageid']]);
if ($database->count('images', ['imagename' => $imagename]) <= 1) {
unlink(FILE_UPLOAD_PATH . "/" . $imagename);
unlink($SETTINGS['file_upload_path'] . "/" . $imagename);
}
$database->delete('images', ['AND' => ['itemid' => $VARS['itemid'], 'imageid' => $VARS['imageid']]]);
@ -361,6 +361,66 @@ switch ($VARS['action']) {
returnToSender("image_deleted", "&id=$VARS[itemid]");
case "signout":
session_destroy();
header('Location: index.php');
header('Location: index.php?logout=1');
die("Logged out.");
case "addstock":
$insert = true;
if (empty($VARS['stock'])) {
$VARS['stock'] = 1;
} else if (!is_numeric($VARS['stock'])) {
returnToSender('field_nan');
}
$user = $_SESSION['uid'];
$data = [
'itemid' => $VARS['itemid'],
'stock' => $VARS['stock'],
'text1' => $VARS['text1'],
'userid' => $user
];
$database->insert('stock', $data);
$currentqty = $database->get('items', 'qty', ['itemid' => $VARS['itemid']]);
$newqty = $currentqty + $VARS['stock'];
$data = [
'qty' => $newqty
];
$database->update('items', $data, ['itemid' => $VARS['itemid']]);
returnToSender("stock_added");
case "removestock":
$insert = true;
if (empty($VARS['stock'])) {
$VARS['stock'] = -1;
} else if (!is_numeric($VARS['stock'])) {
returnToSender('field_nan');
}
$user = $_SESSION['uid'];
$data = [
'itemid' => $VARS['itemid'],
'stock' => -$VARS['stock'],
'text1' => $VARS['text1'],
'userid' => $user
];
$database->insert('stock', $data);
$currentqty = $database->get('items', 'qty', ['itemid' => $VARS['itemid']]);
$newqty = $currentqty - $VARS['stock'];
$data = [
'qty' => $newqty
];
$database->update('items', $data, ['itemid' => $VARS['itemid']]);
returnToSender("stock_removed");
}

33
api.php
View File

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

5
api/.htaccess Normal file
View File

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

9
api/actions/ping.php Normal file
View File

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

15
api/apisettings.php Normal file
View File

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

149
api/functions.php Normal file
View File

@ -0,0 +1,149 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/**
* Build and send a simple JSON response.
* @param string $msg A message
* @param string $status "OK" or "ERROR"
* @param array $data More JSON data
*/
function sendJsonResp(string $msg = null, string $status = "OK", array $data = null) {
$resp = [];
if (!is_null($data)) {
$resp = $data;
}
if (!is_null($msg)) {
$resp["msg"] = $msg;
}
$resp["status"] = $status;
header("Content-Type: application/json");
exit(json_encode($resp));
}
function exitWithJson(array $json) {
header("Content-Type: application/json");
exit(json_encode($json));
}
/**
* Get the API key with most of the characters replaced with *s.
* @global string $key
* @return string
*/
function getCensoredKey() {
global $key;
$resp = $key;
if (strlen($key) > 5) {
for ($i = 2; $i < strlen($key) - 2; $i++) {
$resp[$i] = "*";
}
}
return $resp;
}
/**
* Check if the request is allowed
* @global array $VARS
* @return bool true if the request should continue, false if the request is bad
*/
function authenticate(): bool {
global $VARS, $SETTINGS;
// HTTP basic auth
if (!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW'])) {
$username = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];
} else if (!empty($VARS['username']) && !empty($VARS['password'])) {
$username = $VARS['username'];
$password = $VARS['password'];
} else {
return false;
}
$user = User::byUsername($username);
if (!$user->exists()) {
return false;
}
if ($user->checkPassword($password, true)) {
// Check that the user has permission to access the app
$perms = is_array($SETTINGS['api_permissions']) ? $SETTINGS['api_permissions'] : $SETTINGS['permissions'];
foreach ($perms as $perm) {
if (!$user->hasPermission($perm)) {
return false;
}
}
return true;
}
return false;
}
/**
* Get the User whose credentials were used to make the request.
*/
function getRequestUser(): User {
global $VARS;
if (!empty($_SERVER['PHP_AUTH_USER'])) {
return User::byUsername($_SERVER['PHP_AUTH_USER']);
} else {
return User::byUsername($VARS['username']);
}
}
function checkVars($vars, $or = false) {
global $VARS;
$ok = [];
foreach ($vars as $key => $val) {
if (strpos($key, "OR") === 0) {
checkVars($vars[$key], true);
continue;
}
// Only check type of optional variables if they're set, and don't
// mark them as bad if they're not set
if (strpos($key, " (optional)") !== false) {
$key = str_replace(" (optional)", "", $key);
if (empty($VARS[$key])) {
continue;
}
} else {
if (empty($VARS[$key])) {
$ok[$key] = false;
continue;
}
}
if (strpos($val, "/") === 0) {
// regex
$ok[$key] = preg_match($val, $VARS[$key]) === 1;
} else {
$checkmethod = "is_$val";
$ok[$key] = !($checkmethod($VARS[$key]) !== true);
}
}
if ($or) {
$success = false;
$bad = "";
foreach ($ok as $k => $v) {
if ($v) {
$success = true;
break;
} else {
$bad = $k;
}
}
if (!$success) {
http_response_code(400);
die("400 Bad request: variable $bad is missing or invalid");
}
} else {
foreach ($ok as $key => $bool) {
if (!$bool) {
http_response_code(400);
die("400 Bad request: variable $key is missing or invalid");
}
}
}
}

81
api/index.php Normal file
View File

@ -0,0 +1,81 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
require __DIR__ . '/../required.php';
require __DIR__ . '/functions.php';
require __DIR__ . '/apisettings.php';
header("Access-Control-Allow-Origin: *");
$VARS = $_GET;
if ($_SERVER['REQUEST_METHOD'] != "GET") {
$VARS = array_merge($VARS, $_POST);
}
$requestbody = file_get_contents('php://input');
$requestjson = json_decode($requestbody, TRUE);
if (json_last_error() == JSON_ERROR_NONE) {
$VARS = array_merge($VARS, $requestjson);
}
// If we're not using the old api.php file, allow more flexible requests
if (strpos($_SERVER['REQUEST_URI'], "/api.php") === FALSE) {
$route = explode("/", substr($_SERVER['REQUEST_URI'], strpos($_SERVER['REQUEST_URI'], "api/") + 4));
if (count($route) >= 1) {
$VARS["action"] = $route[0];
}
if (count($route) >= 2 && strpos($route[1], "?") !== 0) {
for ($i = 1; $i < count($route); $i++) {
if (empty($route[$i]) || strpos($route[$i], "=") === false) {
continue;
}
$key = explode("=", $route[$i], 2)[0];
$val = explode("=", $route[$i], 2)[1];
$VARS[$key] = $val;
}
}
if (strpos($route[count($route) - 1], "?") === 0) {
$morevars = explode("&", substr($route[count($route) - 1], 1));
foreach ($morevars as $var) {
$key = explode("=", $var, 2)[0];
$val = explode("=", $var, 2)[1];
$VARS[$key] = $val;
}
}
}
if (!authenticate()) {
header('WWW-Authenticate: Basic realm="' . $SETTINGS['site_title'] . '"');
header('HTTP/1.1 401 Unauthorized');
die("401 Unauthorized: you need to supply valid credentials.");
}
if (empty($VARS['action'])) {
http_response_code(404);
die("404 No action specified");
}
if (!isset($APIS[$VARS['action']])) {
http_response_code(404);
die("404 Action not defined");
}
$APIACTION = $APIS[$VARS["action"]];
if (!file_exists(__DIR__ . "/actions/" . $APIACTION["load"])) {
http_response_code(404);
die("404 Action not found");
}
if (!empty($APIACTION["vars"])) {
checkVars($APIACTION["vars"]);
}
require_once __DIR__ . "/actions/" . $APIACTION["load"];

62
app.php
View File

@ -1,5 +1,4 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@ -14,7 +13,7 @@ if ($_SESSION['loggedin'] != true) {
require_once __DIR__ . "/pages.php";
$pageid = "home";
if (isset($_GET['page']) && !is_empty($_GET['page'])) {
if (!empty($_GET['page'])) {
$pg = strtolower($_GET['page']);
$pg = preg_replace('/[^0-9a-z_]/', "", $pg);
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
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo SITE_TITLE; ?></title>
<title><?php echo $SETTINGS['site_title']; ?></title>
<link rel="icon" href="static/img/logo.svg">
@ -66,28 +65,35 @@ header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", fals
<?php
// Alert messages
if (isset($_GET['msg']) && !is_empty($_GET['msg']) && array_key_exists($_GET['msg'], MESSAGES)) {
// optional string generation argument
if (!isset($_GET['arg']) || is_empty($_GET['arg'])) {
$alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false);
if (!empty($_GET['msg'])) {
if (array_key_exists($_GET['msg'], MESSAGES)) {
// optional string generation argument
if (empty($_GET['arg'])) {
$alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false);
} else {
$alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
}
$alerttype = MESSAGES[$_GET['msg']]['type'];
$alerticon = "square-o";
switch (MESSAGES[$_GET['msg']]['type']) {
case "danger":
$alerticon = "times";
break;
case "warning":
$alerticon = "exclamation-triangle";
break;
case "info":
$alerticon = "info-circle";
break;
case "success":
$alerticon = "check";
break;
}
} else {
$alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
}
$alerttype = MESSAGES[$_GET['msg']]['type'];
$alerticon = "square-o";
switch (MESSAGES[$_GET['msg']]['type']) {
case "danger":
$alerticon = "times";
break;
case "warning":
$alerticon = "exclamation-triangle";
break;
case "info":
$alerticon = "info-circle";
break;
case "success":
$alerticon = "check";
break;
// We don't have a message for this, so just assume an error and escape stuff.
$alertmsg = htmlentities($Strings->get($_GET['msg'], false));
$alerticon = "times";
$alerttype = "danger";
}
echo <<<END
<div class="row justify-content-center" id="msg-alert-box">
@ -121,7 +127,7 @@ END;
</button>
<a class="navbar-brand py-0 mr-auto" href="app.php">
<img src="static/img/logo.svg" alt="" class="d-none d-<?php echo $navbar_breakpoint; ?>-inline brand-img py-0" />
<?php echo SITE_TITLE; ?>
<?php echo $SETTINGS['site_title']; ?>
</a>
<div class="collapse navbar-collapse py-0" id="navbar-collapse">
@ -157,7 +163,7 @@ END;
</div>
<div class="navbar-nav ml-auto py-0" id="navbar-right">
<span class="nav-item py-<?php echo $navbar_breakpoint; ?>-0">
<a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="<?php echo PORTAL_URL; ?>">
<a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="<?php echo $SETTINGS['accounthub']['home']; ?>">
<i class="fas fa-user fa-fw"></i><span>&nbsp;<?php echo $_SESSION['realname'] ?></span>
</a>
</span>
@ -177,8 +183,8 @@ END;
?>
</div>
<div class="footer">
<?php echo FOOTER_TEXT; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
<?php echo $SETTINGS['footer_text']; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo $SETTINGS['copyright']; ?>
</div>
</div>
<script src="static/js/jquery-3.3.1.min.js"></script>

Binary file not shown.

View File

@ -1,5 +1,5 @@
-- MySQL Script generated by MySQL Workbench
-- Sat 22 Sep 2018 02:40:11 AM MDT
-- Wed 11 Mar 2020 10:06:41 EET
-- Model: New Model Version: 1.0
-- MySQL Workbench Forward Engineering
@ -55,14 +55,14 @@ CREATE TABLE IF NOT EXISTS `items` (
`price` DECIMAL(10,2) NULL,
PRIMARY KEY (`itemid`),
INDEX `fk_items_categories_idx` (`catid` ASC),
INDEX `fk_items_locations1_idx` (`locid` ASC),
INDEX `fk_items_locations_idx` (`locid` ASC),
UNIQUE INDEX `itemid_UNIQUE` (`itemid` ASC),
CONSTRAINT `fk_items_categories`
FOREIGN KEY (`catid`)
REFERENCES `categories` (`catid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_items_locations1`
CONSTRAINT `fk_items_locations`
FOREIGN KEY (`locid`)
REFERENCES `locations` (`locid`)
ON DELETE NO ACTION
@ -89,12 +89,12 @@ CREATE TABLE IF NOT EXISTS `permissions` (
`itemid` INT NOT NULL,
`canedit` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`userid`, `itemid`),
INDEX `fk_permissions_items1_idx` (`itemid` ASC),
CONSTRAINT `fk_permissions_items1`
INDEX `fk_permissions_items_idx` (`itemid` ASC),
CONSTRAINT `fk_permissions_items`
FOREIGN KEY (`itemid`)
REFERENCES `items` (`itemid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ON DELETE CASCADE
ON UPDATE CASCADE)
ENGINE = InnoDB;
@ -120,12 +120,33 @@ CREATE TABLE IF NOT EXISTS `images` (
`primary` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`imageid`, `itemid`),
UNIQUE INDEX `imageid_UNIQUE` (`imageid` ASC),
INDEX `fk_images_items1_idx` (`itemid` ASC),
CONSTRAINT `fk_images_items1`
INDEX `fk_images_items_idx` (`itemid` ASC),
CONSTRAINT `fk_images_items`
FOREIGN KEY (`itemid`)
REFERENCES `items` (`itemid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ON DELETE CASCADE
ON UPDATE CASCADE)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `stock`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `stock` (
`stockid` INT NOT NULL AUTO_INCREMENT,
`itemid` INT NOT NULL,
`stock` INT NOT NULL,
`text1` TEXT(500) NOT NULL,
`userid` INT NOT NULL,
`timestamp` TIMESTAMP NOT NULL,
PRIMARY KEY (`stockid`, `itemid`),
UNIQUE INDEX `stockid_UNIQUE` (`stockid` ASC),
INDEX `fk_stock_items_idx` (`itemid` ASC),
CONSTRAINT `fk_stock_items`
FOREIGN KEY (`itemid`)
REFERENCES `items` (`itemid`)
ON DELETE CASCADE
ON UPDATE CASCADE)
ENGINE = InnoDB;

View File

@ -8,7 +8,7 @@
require_once __DIR__ . "/required.php";
$base = FILE_UPLOAD_PATH . "/";
$base = $SETTINGS['file_upload_path'] . "/";
if (isset($_GET['i'])) {
$file = $_GET['i'];
$filepath = $base . $file;
@ -16,7 +16,7 @@ if (isset($_GET['i'])) {
http_response_code(404);
die("404 File Not Found");
}
if (strpos(realpath($filepath), FILE_UPLOAD_PATH) !== 0) {
if (strpos(realpath($filepath), $SETTINGS['file_upload_path']) !== 0) {
http_response_code(404);
die("404 File Not Found");
}

264
index.php
View File

@ -1,175 +1,131 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
require_once __DIR__ . "/required.php";
// if we're logged in, we don't need to be here.
if (!empty($_SESSION['loggedin']) && $_SESSION['loggedin'] === true && !isset($_GET['permissionerror'])) {
header('Location: app.php');
die();
}
if (isset($_GET['permissionerror'])) {
$alert = $Strings->get("no access permission", false);
}
/**
* Show a simple HTML page with a line of text and a button. Matches the UI of
* the AccountHub login flow.
*
* @global type $SETTINGS
* @global type $SECURE_NONCE
* @global type $Strings
* @param string $title Text to show, passed through i18n
* @param string $button Button text, passed through i18n
* @param string $url URL for the button
*/
function showHTML(string $title, string $button, string $url) {
global $SETTINGS, $SECURE_NONCE, $Strings;
?>
<!DOCTYPE html>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
/* Authenticate user */
$userpass_ok = false;
$multiauth = false;
if (Login::checkLoginServer()) {
if (empty($VARS['progress'])) {
// Easy way to remove "undefined" warnings.
} else if ($VARS['progress'] == "1") {
if (!CAPTCHA_ENABLED || (CAPTCHA_ENABLED && Login::verifyCaptcha($VARS['captcheck_session_code'], $VARS['captcheck_selected_answer'], CAPTCHA_SERVER . "/api.php"))) {
$autherror = "";
$user = User::byUsername($VARS['username']);
if ($user->exists()) {
$status = $user->getStatus()->getString();
switch ($status) {
case "LOCKED_OR_DISABLED":
$alert = $Strings->get("account locked", false);
break;
case "TERMINATED":
$alert = $Strings->get("account terminated", false);
break;
case "CHANGE_PASSWORD":
$alert = $Strings->get("password expired", false);
break;
case "NORMAL":
$username_ok = true;
break;
case "ALERT_ON_ACCESS":
$mail_resp = $user->sendAlertEmail();
if (DEBUG) {
var_dump($mail_resp);
}
$username_ok = true;
break;
default:
if (!is_empty($error)) {
$alert = $error;
} else {
$alert = $Strings->get("login error", false);
}
break;
}
if ($username_ok) {
if ($user->checkPassword($VARS['password'])) {
$_SESSION['passok'] = true; // stop logins using only username and authcode
if ($user->has2fa()) {
$multiauth = true;
} else {
Session::start($user);
header('Location: app.php');
die("Logged in, go to app.php");
}
} else {
$alert = $Strings->get("login incorrect", false);
}
}
} else { // User does not exist anywhere
$alert = $Strings->get("login incorrect", false);
}
} else {
$alert = $Strings->get("captcha error", false);
<title><?php echo $SETTINGS['site_title']; ?></title>
<link rel="icon" href="static/img/logo.svg">
<link href="static/css/bootstrap.min.css" rel="stylesheet">
<style nonce="<?php echo $SECURE_NONCE; ?>">
.display-5 {
font-size: 2.5rem;
font-weight: 300;
line-height: 1.2;
}
} else if ($VARS['progress'] == "2") {
$user = User::byUsername($VARS['username']);
if ($_SESSION['passok'] !== true) {
// stop logins using only username and authcode
sendError("Password integrity check failed!");
.banner-image {
max-height: 100px;
margin: 2em auto;
border: 1px solid grey;
border-radius: 15%;
}
if ($user->check2fa($VARS['authcode'])) {
Session::start($user);
header('Location: app.php');
die("Logged in, go to app.php");
} else {
$alert = $Strings->get("2fa incorrect", false);
}
}
} else {
$alert = $Strings->get("login server unavailable", false);
}
header("Link: <static/fonts/Roboto.css>; rel=preload; as=style", false);
header("Link: <static/css/bootstrap.min.css>; rel=preload; as=style", false);
header("Link: <static/css/material-color/material-color.min.css>; rel=preload; as=style", false);
header("Link: <static/css/index.css>; rel=preload; as=style", false);
header("Link: <static/js/jquery-3.3.1.min.js>; rel=preload; as=script", false);
header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", false);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
</style>
<title><?php echo SITE_TITLE; ?></title>
<link rel="icon" href="static/img/logo.svg">
<link href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/material-color/material-color.min.css" rel="stylesheet">
<link href="static/css/index.css" rel="stylesheet">
<?php if (CAPTCHA_ENABLED) { ?>
<script src="<?php echo CAPTCHA_SERVER ?>/captcheck.dist.js"></script>
<?php } ?>
</head>
<body>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-auto">
<img class="banner-image" src="static/img/logo.svg" />
<div class="col-12 text-center">
<img class="banner-image" src="./static/img/logo.svg" />
</div>
</div>
<div class="row justify-content-center">
<div class="card col-11 col-xs-11 col-sm-8 col-md-6 col-lg-4">
<div class="card-body">
<h5 class="card-title"><?php $Strings->get("sign in"); ?></h5>
<form action="" method="POST">
<?php
if (!empty($alert)) {
?>
<div class="alert alert-danger">
<i class="fa fa-fw fa-exclamation-triangle"></i> <?php echo $alert; ?>
</div>
<?php
}
if ($multiauth != true) {
?>
<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 />
<input type="password" class="form-control" name="password" placeholder="<?php $Strings->get("password"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
<?php if (CAPTCHA_ENABLED) { ?>
<div class="captcheck_container" data-stylenonce="<?php echo $SECURE_NONCE; ?>"></div>
<br />
<?php } ?>
<input type="hidden" name="progress" value="1" />
<?php
} else if ($multiauth) {
?>
<div class="alert alert-info">
<?php $Strings->get("2fa prompt"); ?>
</div>
<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 />
<input type="hidden" name="progress" value="2" />
<input type="hidden" name="username" value="<?php echo $VARS['username']; ?>" />
<?php
}
?>
<button type="submit" class="btn btn-primary">
<?php $Strings->get("continue"); ?>
</button>
</form>
<div class="col-12 text-center">
<h1 class="display-5 mb-4"><?php $Strings->get($title); ?></h1>
</div>
<div class="col-12 col-sm-8 col-lg-6">
<div class="card mt-4">
<div class="card-body">
<a href="<?php echo $url; ?>" class="btn btn-primary btn-block"><?php $Strings->get($button); ?></a>
</div>
</div>
</div>
</div>
<div class="footer">
<?php echo FOOTER_TEXT; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
</div>
</div>
<script src="static/js/jquery-3.3.1.min.js"></script>
<script src="static/js/bootstrap.bundle.min.js"></script>
</body>
</html>
<?php
}
if (!empty($_GET['logout'])) {
showHTML("You have been logged out.", "Log in again", "./index.php");
die();
}
if (empty($_SESSION["login_code"])) {
$redirecttologin = true;
} else {
try {
$uidinfo = AccountHubApi::get("checkloginkey", ["code" => $_SESSION["login_code"]]);
if ($uidinfo["status"] == "ERROR") {
throw new Exception();
}
if (is_numeric($uidinfo['uid'])) {
$user = new User($uidinfo['uid'] * 1);
foreach ($SETTINGS['permissions'] as $perm) {
if (!$user->hasPermission($perm)) {
showHTML("no access permission", "sign out", "./action.php?action=signout");
die();
}
}
Session::start($user);
$_SESSION["login_code"] = null;
header('Location: app.php');
showHTML("Logged in", "Continue", "./app.php");
die();
} else {
throw new Exception();
}
} catch (Exception $ex) {
$redirecttologin = true;
}
}
if ($redirecttologin) {
try {
$urlbase = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . (($_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) ? ":" . $_SERVER['SERVER_PORT'] : "");
$iconurl = $urlbase . str_replace("index.php", "", $_SERVER["REQUEST_URI"]) . "static/img/logo.svg";
$codedata = AccountHubApi::get("getloginkey", ["appname" => $SETTINGS["site_title"], "appicon" => $iconurl]);
if ($codedata['status'] != "OK") {
throw new Exception($Strings->get("login server unavailable", false));
}
$redirecturl = $urlbase . $_SERVER['REQUEST_URI'];
$_SESSION["login_code"] = $codedata["code"];
$locationurl = $codedata["loginurl"] . "?code=" . htmlentities($codedata["code"]) . "&redirect=" . htmlentities($redirecturl);
header("Location: $locationurl");
showHTML("Continue", "Continue", $locationurl);
die();
} catch (Exception $ex) {
sendError($ex->getMessage());
}
}

View File

@ -1,114 +0,0 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
define("STRINGS", [
"sign in" => "Sign In",
"username" => "Username",
"password" => "Password",
"continue" => "Continue",
"authcode" => "Authentication code",
"2fa prompt" => "Enter the six-digit code from your mobile authenticator app.",
"2fa incorrect" => "Authentication code incorrect.",
"login incorrect" => "Login incorrect.",
"login server unavailable" => "Login server unavailable. Try again later or contact technical support.",
"account locked" => "This account has been disabled. Contact technical support.",
"password expired" => "You must change your password before continuing.",
"account terminated" => "Account terminated. Access denied.",
"account state error" => "Your account state is not stable. Log out, restart your browser, and try again.",
"welcome user" => "Welcome, {user}!",
"no permission" => "You do not have permission to access this system.",
"sign out" => "Sign out",
"settings" => "Settings",
"options" => "Options",
"404 error" => "404 Error",
"page not found" => "Page not found.",
"invalid parameters" => "Invalid request parameters.",
"login server error" => "The login server returned an error: {arg}",
"login server user data error" => "The login server refused to provide account information. Try again or contact technical support.",
"captcha error" => "There was a problem with the CAPTCHA (robot test). Try again.",
"no edit permission" => "You do not have permission to modify records.",
"no access permission" => "You do not have permission to access this system.",
"home" => "Home",
"more" => "More",
"invalid itemid" => "The item ID is invalid.",
"invalid category" => "The category is invalid.",
"invalid location" => "The location does not exist.",
"item saved" => "Item saved.",
"item deleted" => "Item deleted.",
"total items" => "Total Items",
"items" => "Items",
"locations" => "Locations",
"categories" => "Categories",
"actions" => "Actions",
"name" => "Name",
"category" => "Category",
"location" => "Location",
"code" => "Code",
"code 1" => "Code 1",
"code 2" => "Code 2",
"qty" => "Qty",
"quantity" => "Quantity",
"assigned to" => "Assigned To",
"view items" => "View Items",
"nobody" => "Nobody",
"view categories" => "View Categories",
"view locations" => "View Locations",
"edit" => "Edit",
"clone" => "Copy",
"item count" => "Items",
"delete" => "Delete",
"new item" => "New Item",
"editing item" => "Editing {item}",
"editing category" => "Editing {cat}",
"editing location" => "Editing {loc}",
"cloning item" => "Copying {oitem} <i class=\"fa fa-angle-right\"></i> {nitem}",
"adding item" => "Adding new item",
"adding category" => "Adding new category",
"invalid catid" => "Invalid category ID.",
"category deleted" => "Category deleted.",
"category in use" => "Cannot delete category because there is at least one item still in it.",
"new category" => "New Category",
"category saved" => "Category saved.",
"adding location" => "Adding new location",
"invalid locid" => "Invalid location ID.",
"location deleted" => "Location deleted.",
"location in use" => "Cannot delete location because there is at least one item still in it.",
"new location" => "New Location",
"location saved" => "Location saved.",
"name" => "Name",
"save" => "Save",
"placeholder item name" => "Foo Bar",
"placeholder category name" => "Widgets",
"placeholder location name" => "Over the Hills",
"description" => "Description",
"notes" => "Notes",
"comments" => "Comments",
"minwant" => "Minimum On Hand",
"want" => "Need",
"field not a number" => "You entered something that isn't a number when a number was expected.",
"understocked items" => "Understocked Items",
"view understocked" => "View Understocked",
"only showing understocked" => "Only showing understocked items.",
"show all items" => "Show all items",
"missing name" => "You need to enter a name.",
"use the dropdowns" => "Whoops, you need to use the category and location autocomplete boxes.",
"make categories and locations" => "Please create at least one category and location before adding an item.",
"search" => "Search Items",
"report export" => "Reports/Export",
"report type" => "Report type",
"format" => "Format",
"generate report" => "Generate report",
"choose an option" => "Choose an option",
"csv file" => "CSV text file",
"ods file" => "ODS spreadsheet",
"html file" => "HTML web page",
"itemid" => "Item ID",
"id" => "ID",
"item cost" => "Item cost",
"sale price" => "Sale price",
"cost" => "Cost",
"price" => "Price"
]);

View File

@ -4,5 +4,7 @@
"save": "Save",
"delete": "Delete",
"view": "View",
"show all items": "Show All Items"
"show all items": "Show All Items",
"addstock": "Add",
"removestock": "Remove"
}

View File

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

View File

@ -6,5 +6,7 @@
"Promoted": "Promoted",
"Promote": "Promote",
"Delete": "Delete",
"Back": "Back"
"Back": "Back",
"Image uploaded.": "Image uploaded.",
"Upload finished with errors: {arg}": "Upload finished with errors: {arg}"
}

8
langs/en/index.json Normal file
View File

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

View File

@ -9,5 +9,8 @@
"cloning item": "Copying {oitem} <i class=\"fa fa-angle-right\"></i> {nitem}",
"itemid": "Item ID",
"id": "ID",
"Edit Item": "Edit Item"
"Edit Item": "Edit Item",
"stockid": "Stock ID",
"adding stock": "Adding stock for {item}",
"removing stock": "Removing stock from {item}"
}

View File

@ -7,10 +7,10 @@
"code 1": "Code 1",
"code 2": "Code 2",
"qty": "Qty",
"want": "Need",
"assigned to": "Assigned To",
"want": "Min",
"assigned to": "Assigned to",
"quantity": "Quantity",
"minwant": "Minimum On Hand",
"minwant": "Minimum on hand",
"item count": "Item count",
"Item cost": "Item cost",
"Sale price": "Sale price",
@ -18,5 +18,9 @@
"Notes": "Notes",
"Comments": "Comments",
"Cost": "Cost",
"Price": "Price"
"Price": "Price",
"date": "Date",
"amount": "Stock amount",
"description": "Description",
"changed by": "Changed by"
}

View File

@ -17,5 +17,7 @@
"only showing understocked": "Only showing understocked items.",
"missing name": "You need to enter a name.",
"use the dropdowns": "Whoops, you need to use the category and location autocomplete boxes.",
"make categories and locations": "Please create at least one category and location before adding an item."
"make categories and locations": "Please create at least one category and location before adding an item.",
"stock added": "Stock added.",
"stock removed": "Stock removed."
}

View File

@ -5,5 +5,6 @@
"choose an option": "Choose an option",
"csv file": "CSV text file",
"ods file": "ODS spreadsheet",
"html file": "HTML web page"
"html file": "HTML web page",
"Stock": "Stock movements"
}

View File

@ -88,5 +88,21 @@ define("MESSAGES", [
"noloccat" => [
"string" => "make categories and locations",
"type" => "info"
],
"upload_warning" => [
"string" => "Upload finished with errors: {arg}",
"type" => "warning"
],
"upload_success" => [
"string" => "Image uploaded.",
"type" => "success"
],
"stock_added" => [
"string" => "stock added",
"type" => "success"
],
"stock_removed" => [
"string" => "stock removed",
"type" => "success"
]
]);

56
lib/AccountHubApi.lib.php Normal file
View File

@ -0,0 +1,56 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
class AccountHubApi {
public static function get(string $action, array $data = null, bool $throwex = false) {
global $SETTINGS;
$content = [
"action" => $action,
"key" => $SETTINGS['accounthub']['key']
];
if (!is_null($data)) {
$content = array_merge($content, $data);
}
$options = [
'http' => [
'method' => 'POST',
'content' => json_encode($content),
'header' => "Content-Type: application/json\r\n" .
"Accept: application/json\r\n",
"ignore_errors" => true
]
];
$context = stream_context_create($options);
$result = file_get_contents($SETTINGS['accounthub']['api'], false, $context);
$response = json_decode($result, true);
if ($result === false || !AccountHubApi::checkHttpRespCode($http_response_header) || json_last_error() != JSON_ERROR_NONE) {
if ($throwex) {
throw new Exception($result);
} else {
sendError($result);
}
}
return $response;
}
private static function checkHttpRespCode(array $headers): bool {
foreach ($headers as $header) {
if (preg_match("/HTTP\/[0-9]\.[0-9] [0-9]{3}.*/", $header)) {
$respcode = explode(" ", $header)[1] * 1;
if ($respcode >= 200 && $respcode < 300) {
return true;
}
}
}
return false;
}
}

326
lib/FormBuilder.lib.php Normal file
View File

@ -0,0 +1,326 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
class FormBuilder {
private $items = [];
private $hiddenitems = [];
private $title = "";
private $icon = "";
private $buttons = [];
private $action = "action.php";
private $method = "POST";
private $id = "editform";
/**
* Create a form with autogenerated HTML.
*
* @param string $title Form title/heading
* @param string $icon FontAwesone icon next to the title.
* @param string $action URL to submit the form to.
* @param string $method Form submission method (POST, GET, etc.)
*/
public function __construct(string $title = "Untitled Form", string $icon = "fas fa-file-alt", string $action = "action.php", string $method = "POST") {
$this->title = $title;
$this->icon = $icon;
$this->action = $action;
$this->method = $method;
}
/**
* Set the title of the form.
* @param string $title
*/
public function setTitle(string $title) {
$this->title = $title;
}
/**
* Set the icon for the form.
* @param string $icon FontAwesome icon (example: "fas fa-toilet-paper")
*/
public function setIcon(string $icon) {
$this->icon = $icon;
}
/**
* Set the URL the form will submit to.
* @param string $action
*/
public function setAction(string $action) {
$this->action = $action;
}
/**
* Set the form submission method (GET, POST, etc)
* @param string $method
*/
public function setMethod(string $method = "POST") {
$this->method = $method;
}
/**
* Set the form ID.
* @param string $id
*/
public function setID(string $id = "editform") {
$this->id = $id;
}
/**
* Add an input to the form.
*
* @param string $name Element name
* @param string $value Element value
* @param string $type Input type (text, number, date, select, tel...)
* @param bool $required If the element is required for form submission.
* @param string $id Element ID
* @param array $options Array of [value => text] pairs for a select element
* @param string $label Text label to display near the input
* @param string $icon FontAwesome icon (example: "fas fa-toilet-paper")
* @param int $width Bootstrap column width for the input, out of 12.
* @param int $minlength Minimum number of characters for the input.
* @param int $maxlength Maximum number of characters for the input.
* @param string $pattern Regex pattern for custom client-side validation.
* @param string $error Message to show if the input doesn't validate.
*/
public function 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 = "") {
$item = [
"name" => $name,
"value" => $value,
"type" => $type,
"required" => $required,
"label" => $label,
"icon" => $icon,
"width" => $width,
"minlength" => $minlength,
"maxlength" => $maxlength
];
if (!empty($id)) {
$item["id"] = $id;
}
if (!empty($options) && $type == "select") {
$item["options"] = $options;
}
if (!empty($pattern)) {
$item["pattern"] = $pattern;
}
if (!empty($error)) {
$item["error"] = $error;
}
$this->items[] = $item;
}
/**
* Add a text input.
*
* @param string $name Element name
* @param string $value Element value
* @param bool $required If the element is required for form submission.
* @param string $id Element ID
* @param string $label Text label to display near the input
* @param string $icon FontAwesome icon (example: "fas fa-toilet-paper")
* @param int $width Bootstrap column width for the input, out of 12.
* @param int $minlength Minimum number of characters for the input.
* @param int $maxlength Maximum number of characters for the input.
* @param string $pattern Regex pattern for custom client-side validation.
* @param string $error Message to show if the input doesn't validate.
*/
public function addTextInput(string $name, string $value = "", bool $required = true, string $id = "", string $label = "", string $icon = "", int $width = 4, int $minlength = 1, int $maxlength = 100, string $pattern = "", string $error = "") {
$this->addInput($name, $value, "text", $required, $id, null, $label, $icon, $width, $minlength, $maxlength, $pattern, $error);
}
/**
* Add a select dropdown.
*
* @param string $name Element name
* @param string $value Element value
* @param bool $required If the element is required for form submission.
* @param string $id Element ID
* @param array $options Array of [value => text] pairs for a select element
* @param string $label Text label to display near the input
* @param string $icon FontAwesome icon (example: "fas fa-toilet-paper")
* @param int $width Bootstrap column width for the input, out of 12.
*/
public function addSelect(string $name, string $value = "", bool $required = true, string $id = null, array $options = null, string $label = "", string $icon = "", int $width = 4) {
$this->addInput($name, $value, "select", $required, $id, $options, $label, $icon, $width);
}
/**
* Add a button to the form.
*
* @param string $text Text string to show on the button.
* @param string $icon FontAwesome icon to show next to the text.
* @param string $href If not null, the button will actually be a hyperlink.
* @param string $type Usually "button" or "submit". Ignored if $href is set.
* @param string $id The element ID.
* @param string $name The element name for the button.
* @param string $value The form value for the button. Ignored if $name is null.
* @param string $class The CSS classes for the button, if a standard success-colored one isn't right.
*/
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") {
$button = [
"text" => $text,
"icon" => $icon,
"class" => $class,
"type" => $type,
"id" => $id,
"href" => $href,
"name" => $name,
"value" => $value
];
$this->buttons[] = $button;
}
/**
* Add a hidden input.
* @param string $name
* @param string $value
*/
public function addHiddenInput(string $name, string $value) {
$this->hiddenitems[$name] = $value;
}
/**
* Generate the form HTML.
* @param bool $echo If false, returns HTML string instead of outputting it.
*/
public function generate(bool $echo = true) {
$html = <<<HTMLTOP
<form action="$this->action" method="$this->method" id="$this->id">
<div class="card">
<h3 class="card-header d-flex">
<div>
<i class="$this->icon"></i> $this->title
</div>
</h3>
<div class="card-body">
<div class="row">
HTMLTOP;
foreach ($this->items as $item) {
$required = $item["required"] ? "required" : "";
$id = empty($item["id"]) ? "" : "id=\"$item[id]\"";
$pattern = empty($item["pattern"]) ? "" : "pattern=\"$item[pattern]\"";
if (empty($item['type'])) {
$item['type'] = "text";
}
$itemhtml = "";
$itemlabel = "";
if ($item['type'] == "textarea") {
$itemlabel = "<label class=\"mb-0\"><i class=\"$item[icon]\"></i> $item[label]:</label>";
} else if ($item['type'] != "checkbox") {
$itemlabel = "<label class=\"mb-0\">$item[label]:</label>";
}
$strippedlabel = strip_tags($item['label']);
$itemhtml .= <<<ITEMTOP
\n\n <div class="col-12 col-md-$item[width]">
<div class="form-group mb-3">
$itemlabel
ITEMTOP;
$inputgrouptop = <<<INPUTG
\n <div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="$item[icon]"></i></span>
</div>
INPUTG;
switch ($item['type']) {
case "select":
$itemhtml .= $inputgrouptop;
$itemhtml .= <<<SELECT
\n <select class="form-control" name="$item[name]" aria-label="$strippedlabel" $required>
SELECT;
foreach ($item['options'] as $value => $label) {
$selected = "";
if (!empty($item['value']) && $value == $item['value']) {
$selected = " selected";
}
$itemhtml .= "\n <option value=\"$value\"$selected>$label</option>";
}
$itemhtml .= "\n </select>";
break;
case "checkbox":
$itemhtml .= $inputgrouptop;
$itemhtml .= <<<CHECKBOX
\n <div class="form-group form-check">
<input type="checkbox" name="$item[name]" $id class="form-check-input" value="$item[value]" $required aria-label="$strippedlabel">
<label class="form-check-label">$item[label]</label>
</div>
CHECKBOX;
break;
case "textarea":
$val = htmlentities($item['value']);
$itemhtml .= <<<TEXTAREA
\n <textarea class="form-control" id="info" name="$item[name]" aria-label="$strippedlabel" minlength="$item[minlength]" maxlength="$item[maxlength]" $required>$val</textarea>
TEXTAREA;
break;
default:
$itemhtml .= $inputgrouptop;
$itemhtml .= <<<INPUT
\n <input type="$item[type]" name="$item[name]" $id class="form-control" aria-label="$strippedlabel" minlength="$item[minlength]" maxlength="$item[maxlength]" $pattern value="$item[value]" $required />
INPUT;
break;
}
if (!empty($item["error"])) {
$itemhtml .= <<<ERROR
\n <div class="invalid-feedback">
$item[error]
</div>
ERROR;
}
if ($item["type"] != "textarea") {
$itemhtml .= "\n </div>";
}
$itemhtml .= <<<ITEMBOTTOM
\n </div>
</div>\n
ITEMBOTTOM;
$html .= $itemhtml;
}
$html .= <<<HTMLBOTTOM
</div>
</div>
HTMLBOTTOM;
if (!empty($this->buttons)) {
$html .= "\n <div class=\"card-footer d-flex\">";
foreach ($this->buttons as $btn) {
$btnhtml = "";
$inner = "<i class=\"$btn[icon]\"></i> $btn[text]";
$id = empty($btn['id']) ? "" : "id=\"$btn[id]\"";
if (!empty($btn['href'])) {
$btnhtml = "<a href=\"$btn[href]\" class=\"$btn[class]\" $id>$inner</a>";
} else {
$name = empty($btn['name']) ? "" : "name=\"$btn[name]\"";
$value = (!empty($btn['name']) && !empty($btn['value'])) ? "value=\"$btn[value]\"" : "";
$btnhtml = "<button type=\"$btn[type]\" class=\"$btn[class]\" $id $name $value>$inner</button>";
}
$html .= "\n $btnhtml";
}
$html .= "\n </div>";
}
$html .= "\n </div>";
foreach ($this->hiddenitems as $name => $value) {
$value = htmlentities($value);
$html .= "\n <input type=\"hidden\" name=\"$name\" value=\"$value\" />";
}
$html .= "\n</form>\n";
if ($echo) {
echo $html;
}
return $html;
}
}

View File

@ -45,50 +45,13 @@ class Login {
return Login::LOGIN_OK;
}
public static function verifyCaptcha(string $session, string $answer, string $url): bool {
$data = [
'session_id' => $session,
'answer_id' => $answer,
'action' => "verify"
];
$options = [
'http' => [
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($data)
]
];
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
$resp = json_decode($result, TRUE);
if (!$resp['result']) {
return false;
} else {
return true;
}
}
/**
* Check the login server API for sanity
* @return boolean true if OK, else false
*/
public static function checkLoginServer() {
try {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "ping"
]
]);
if ($response->getStatusCode() != 200) {
return false;
}
$resp = json_decode($response->getBody(), TRUE);
$resp = AccountHubApi::get("ping");
if ($resp['status'] == "OK") {
return true;
} else {
@ -107,19 +70,7 @@ class Login {
*/
function checkAPIKey($key) {
try {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => $key,
'action' => "ping"
]
]);
if ($response->getStatusCode() === 200) {
return true;
}
$resp = AccountHubApi::get("ping", null, true);
return false;
} catch (Exception $e) {
return false;

View File

@ -32,27 +32,15 @@ class Notifications {
$timestamp = date("Y-m-d H:i:s", strtotime($timestamp));
}
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "addnotification",
'uid' => $user->getUID(),
'title' => $title,
'content' => $content,
'timestamp' => $timestamp,
'url' => $url,
'sensitive' => $sensitive
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
$resp = AccountHubApi::get("addnotification", [
'uid' => $user->getUID(),
'title' => $title,
'content' => $content,
'timestamp' => $timestamp,
'url' => $url,
'sensitive' => $sensitive
]
);
if ($resp['status'] == "OK") {
return $resp['id'] * 1;
} else {

View File

@ -21,6 +21,10 @@ class Strings {
$this->load("en");
if ($language == "en") {
return;
}
if (file_exists(__DIR__ . "/../langs/$language/")) {
$this->language = $language;
$this->load($language);

View File

@ -17,22 +17,7 @@ class User {
public function __construct(int $uid, string $username = "") {
// Check if user exists
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "userexists",
'uid' => $uid
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
$resp = AccountHubApi::get("userexists", ["uid" => $uid]);
if ($resp['status'] == "OK" && $resp['exists'] === true) {
$this->exists = true;
} else {
@ -43,22 +28,7 @@ class User {
if ($this->exists) {
// Get user info
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "userinfo",
'uid' => $uid
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
$resp = AccountHubApi::get("userinfo", ["uid" => $uid]);
if ($resp['status'] == "OK") {
$this->uid = $resp['data']['uid'] * 1;
$this->username = $resp['data']['username'];
@ -71,22 +41,7 @@ class User {
}
public static function byUsername(string $username): User {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'username' => $username,
'action' => "userinfo"
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
$resp = AccountHubApi::get("userinfo", ["username" => $username]);
if (!isset($resp['status'])) {
sendError("Login server error: " . $resp);
}
@ -105,22 +60,8 @@ class User {
if (!$this->exists) {
return false;
}
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "hastotp",
'username' => $this->username
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
$resp = AccountHubApi::get("hastotp", ['username' => $this->username]);
if ($resp['status'] == "OK") {
return $resp['otp'] == true;
} else {
@ -147,26 +88,11 @@ class User {
/**
* Check the given plaintext password against the stored hash.
* @param string $password
* @param bool $apppass Set to true to enforce app passwords when 2fa is on.
* @return bool
*/
function checkPassword(string $password): bool {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "auth",
'username' => $this->username,
'password' => $password
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
function checkPassword(string $password, bool $apppass = false): bool {
$resp = AccountHubApi::get("auth", ['username' => $this->username, 'password' => $password, 'apppass' => ($apppass ? "1" : "0")]);
if ($resp['status'] == "OK") {
return true;
} else {
@ -174,27 +100,13 @@ class User {
}
}
function check2fa(string $code): bool {
if (!$this->has2fa) {
return true;
}
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "verifytotp",
'username' => $this->username,
'code' => $code
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
$resp = AccountHubApi::get("verifytotp", ['username' => $this->username, 'code' => $code]);
if ($resp['status'] == "OK") {
return $resp['valid'];
} else {
@ -209,23 +121,7 @@ class User {
* @return boolean TRUE if the user has the permission (or admin access), else FALSE
*/
function hasPermission(string $code): bool {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "permission",
'username' => $this->username,
'code' => $code
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
$resp = AccountHubApi::get("permission", ['username' => $this->username, 'code' => $code]);
if ($resp['status'] == "OK") {
return $resp['has_permission'];
} else {
@ -238,23 +134,7 @@ class User {
* @return \AccountStatus
*/
function getStatus(): AccountStatus {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "acctstatus",
'username' => $this->username
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
$resp = AccountHubApi::get("acctstatus", ['username' => $this->username]);
if ($resp['status'] == "OK") {
return AccountStatus::fromString($resp['account']);
} else {
@ -262,24 +142,13 @@ class User {
}
}
function sendAlertEmail(string $appname = SITE_TITLE) {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "alertemail",
'username' => $this->username,
'appname' => SITE_TITLE
]
]);
if ($response->getStatusCode() > 299) {
return "An unknown error occurred.";
function sendAlertEmail(string $appname = null) {
global $SETTINGS;
if (is_null($appname)) {
$appname = $SETTINGS['site_title'];
}
$resp = AccountHubApi::get("alertemail", ['username' => $this->username, 'appname' => $SETTINGS['site_title']]);
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return true;
} else {

View File

@ -32,32 +32,32 @@ if ($VARS['order'][0]['dir'] == 'asc') {
$sortby = "ASC";
}
switch ($VARS['order'][0]['column']) {
case 2:
case 1:
$order = ["name" => $sortby];
break;
case 3:
case 2:
$order = ["catname" => $sortby];
break;
case 4:
case 3:
$order = ["locname" => $sortby];
break;
case 5:
case 4:
$order = ["code1" => $sortby];
break;
case 6:
case 5:
$order = ["code2" => $sortby];
break;
case 7:
case 6:
$order = ["qty" => $sortby];
break;
case 8:
case 7:
$order = ["want" => $sortby];
break;
// Note: We're not going to sort by assigned user. It's too hard. Maybe later.
}
// search
if (!is_empty($VARS['search']['value'])) {
if (!empty($VARS['search']['value'])) {
$filter = true;
$wherenolimit = [];
if ($showwant) {
@ -118,8 +118,17 @@ for ($i = 0; $i < count($items); $i++) {
$user = new User($_SESSION['uid']);
if ($user->hasPermission("INV_EDIT")) {
$items[$i]["editbtn"] = '<a class="btn btn-primary" href="app.php?page=edititem&id=' . $items[$i]['itemid'] . '"><i class="fas fa-edit"></i> ' . $Strings->get("edit", false) . '</a>';
if ($SETTINGS['stock_management']) {
$items[$i]["addstockbtn"] = '<a class="btn btn-success" href="app.php?page=addstock&id=' . $items[$i]['itemid'] . '"><i class="fas fa-plus"></i> ' . $Strings->get("addstock", false) . '</a>';
$items[$i]["removestockbtn"] = '<a class="btn btn-danger" href="app.php?page=removestock&id=' . $items[$i]['itemid'] . '"><i class="fas fa-minus"></i> ' . $Strings->get("removestock", false) . '</a>';
} else {
$items[$i]["addstockbtn"] = '';
$items[$i]["removestockbtn"] = '';
}
} else {
$items[$i]["editbtn"] = '';
$items[$i]["addstockbtn"] = '';
$items[$i]["removestockbtn"] = '';
}
$items[$i]["viewbtn"] = '<a class="btn btn-info" href="app.php?page=item&id=' . $items[$i]['itemid'] . '"><i class="fas fa-eye"></i> ' . $Strings->get("view", false) . '</a>';
if (is_null($items[$i]['userid'])) {

View File

@ -75,7 +75,7 @@ function getItemReport($filter = []): Report {
$Strings->get("code 1", false),
$Strings->get("code 2", false),
$Strings->get("quantity", false),
$Strings->get("want", false),
$Strings->get("minwant", false),
$Strings->get("Cost", false),
$Strings->get("Price", false),
$Strings->get("assigned to", false),
@ -151,6 +151,49 @@ function getLocationReport(): Report {
return $report;
}
function getStockReport($filter = []): Report {
global $database, $Strings;
$stock = $database->select(
"stock", [
"[>]items" => ["itemid"]
], [
"stockid",
"itemid",
"name",
'timestamp',
'stock',
'stock.text1',
'stock.userid'
]);
$report = new Report($Strings->get("Stock", false));
$report->setHeader([
$Strings->get("stockid", false),
$Strings->get("itemid", false),
$Strings->get("name", false),
$Strings->get("date", false),
$Strings->get("amount", false),
$Strings->get("description", false),
$Strings->get("changed by", false)
]);
for ($i = 0; $i < count($stock); $i++) {
$user = "";
if (!is_null($stock[$i]["userid"])) {
$u = new User($stock[$i]["userid"]);
$user = $u->getName() . " (" . $u->getUsername() . ')';
}
$report->addDataRow([
$stock[$i]["stockid"],
$stock[$i]["itemid"],
$stock[$i]["name"],
$stock[$i]["timestamp"],
$stock[$i]["stock"],
$stock[$i]["text1"],
$user
]);
}
return $report;
}
function getReport($type): Report {
switch ($type) {
case "item":
@ -165,6 +208,9 @@ function getReport($type): Report {
case "itemstock":
return getItemReport(["AND" => ["qty[<]want", "want[>]" => 0]]);
break;
case "stock":
return getStockReport();
break;
default:
return new Report("error", ["ERROR"], ["Invalid report type."]);
}
@ -173,4 +219,4 @@ function getReport($type): Report {
function generateReport($type, $format) {
$report = getReport($type);
$report->output($format);
}
}

View File

@ -8,10 +8,6 @@
* Mobile app API
*/
// The name of the permission needed to log in.
// Set to null if you don't need it.
$access_permission = "INV_VIEW";
require __DIR__ . "/../required.php";
header('Content-Type: application/json');
@ -23,21 +19,7 @@ if ($VARS['action'] == "ping") {
}
function mobile_enabled() {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "mobileenabled"
]
]);
if ($response->getStatusCode() > 299) {
return false;
}
$resp = json_decode($response->getBody(), TRUE);
$resp = AccountHubApi::get("mobileenabled");
if ($resp['status'] == "OK" && $resp['mobile'] === TRUE) {
return true;
} else {
@ -46,26 +28,15 @@ function mobile_enabled() {
}
function mobile_valid($username, $code) {
$client = new GuzzleHttp\Client();
try {
$resp = AccountHubApi::get("mobilevalid", ["code" => $code, "username" => $username], true);
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
"code" => $code,
"username" => $username,
'action' => "mobilevalid"
]
]);
if ($response->getStatusCode() > 299) {
return false;
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK" && $resp['valid'] === TRUE) {
return true;
} else {
if ($resp['status'] == "OK" && $resp['valid'] === TRUE) {
return true;
} else {
return false;
}
} catch (Exception $ex) {
return false;
}
}
@ -75,7 +46,7 @@ if (mobile_enabled() !== TRUE) {
}
// Make sure we have a username and access key
if (is_empty($VARS['username']) || is_empty($VARS['key'])) {
if (empty($VARS['username']) || empty($VARS['key'])) {
http_response_code(401);
die(json_encode(["status" => "ERROR", "msg" => "Missing username and/or access key."]));
}
@ -95,13 +66,14 @@ switch ($VARS['action']) {
if ($user->exists()) {
if ($user->getStatus()->getString() == "NORMAL") {
if ($user->checkPassword($VARS['password'])) {
if (is_null($access_permission) || $user->hasPermission($access_permission)) {
Session::start($user);
$_SESSION['mobile'] = true;
exit(json_encode(["status" => "OK"]));
} else {
exit(json_encode(["status" => "ERROR", "msg" => lang("no permission", false)]));
foreach ($SETTINGS['permissions'] as $perm) {
if (!$user->hasPermission($perm)) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("no permission", false)]));
}
}
Session::start($user);
$_SESSION['mobile'] = true;
exit(json_encode(["status" => "OK"]));
}
}
}

View File

@ -24,19 +24,6 @@ define("PAGES", [
"static/js/items.js"
],
],
"locations" => [
"title" => "Locations",
"navbar" => true,
"icon" => "fas fa-map-marker",
"styles" => [
"static/css/datatables.min.css",
"static/css/tables.css"
],
"scripts" => [
"static/js/datatables.min.js",
"static/js/locations.js"
],
],
"categories" => [
"title" => "Categories",
"navbar" => true,
@ -50,6 +37,19 @@ define("PAGES", [
"static/js/categories.js"
],
],
"locations" => [
"title" => "Locations",
"navbar" => true,
"icon" => "fas fa-map-marker",
"styles" => [
"static/css/datatables.min.css",
"static/css/tables.css"
],
"scripts" => [
"static/js/datatables.min.js",
"static/js/locations.js"
],
],
"item" => [
"title" => "Item",
"navbar" => false
@ -99,5 +99,27 @@ define("PAGES", [
],
"404" => [
"title" => "404 error"
],
"addstock" => [
"title" => "Add stock",
"navbar" => false,
"styles" => [
"static/css/easy-autocomplete.min.css"
],
"scripts" => [
"static/js/jquery.easy-autocomplete.min.js",
"static/js/edititem.js"
],
],
"removestock" => [
"title" => "Remove stock",
"navbar" => false,
"styles" => [
"static/css/easy-autocomplete.min.css"
],
"scripts" => [
"static/js/jquery.easy-autocomplete.min.js",
"static/js/edititem.js"
],
]
]);

77
pages/addstock.php Normal file
View File

@ -0,0 +1,77 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
require_once __DIR__ . '/../required.php';
redirectifnotloggedin();
if ($database->count("locations") == 0 || $database->count("categories") == 0) {
header('Location: app.php?page=items&msg=noloccat');
die();
}
$itemdata = [
'text1' => '',
'qty' => ''];
if (!empty($VARS['id'])) {
if ($database->has('items', ['itemid' => $VARS['id']])) {
$itemdata = $database->select(
'items', [
'name',
'qty',
], [
'itemid' => $VARS['id']
])[0];
} else {
// item id is invalid, redirect to a page that won't cause an error when pressing Save
header('Location: app.php?page=addstock');
die();
}
}
?>
<form role="form" action="action.php" method="POST">
<div class="card border-green">
<h3 class="card-header text-green">
<i class="fas fa-edit"></i> <?php $Strings->build("adding stock", ['item' => "<span id=\"name_title\">" . htmlspecialchars($itemdata['name']) . "</span>"]); ?>
</h3>
<div class="card-body">
<div class="row">
<div class="col-12 col-md-3">
<div class="form-group">
<label for="stock"><i class="fas fa-hashtag"></i> <?php $Strings->get('quantity'); ?></label>
<input type="number" min="1" class="form-control" id="stock" name="stock" required="required" placeholder="<?php echo $itemdata['qty']; ?>" />
</div>
</div>
</div>
<div class="row">
<div class="col-12 col-md-6">
<div class="form-group">
<label for="info1"><i class="fas fa-info"></i> <?php $Strings->get("Description"); ?></label>
<textarea class="form-control" id="info1" name="text1" required="required"></textarea>
</div>
</div>
</div>
</div>
<input type="hidden" name="itemid" value="<?php
echo htmlspecialchars($VARS['id']);
?>" />
<input type="hidden" name="action" value="addstock" />
<?php
if (isset($_GET['source']) && $_GET['source'] === "item") {
echo '<input type="hidden" name="source" value="item" />';
} else {
echo '<input type="hidden" name="source" value="items" />';
}
?>
<div class="card-footer d-flex">
<button type="submit" class="btn btn-success mr-1"><i class="fas fa-save"></i> <?php $Strings->get("save"); ?></button>
</div>
</div>
</form>

View File

@ -14,9 +14,9 @@ redirectifnotloggedin();
<thead>
<tr>
<th data-priority="0"></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
<th data-priority="1"><i class="fas fa-pallet hidden-sm"></i> <?php $Strings->get('category'); ?></th>
<th data-priority="2"><i class="fas fa-hashtag hidden-sm"></i> <?php $Strings->get('item count'); ?></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
</tr>
</thead>
<tbody>
@ -30,11 +30,11 @@ redirectifnotloggedin();
?>
<tr>
<td></td>
<td>
<a class="btn btn-primary btn-sm" href="app.php?page=editcat&id=<?php echo $cat['catid']; ?>"><i class="fas fa-edit"></i> <?php $Strings->get("edit"); ?></a>
</td>
<td><?php echo $cat['catname']; ?></td>
<td><?php echo $itemcount; ?></td>
<td>
<a class="btn btn-primary btn-sm" href="app.php?page=editcat&id=<?php echo $cat['catid']; ?>"><i class="fas fa-edit"></i> <?php $Strings->get("edit"); ?></a>
</td>
</tr>
<?php
}
@ -43,9 +43,9 @@ redirectifnotloggedin();
<tfoot>
<tr>
<th data-priority="0"></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
<th data-priority="1"><i class="fas fa-pallet hidden-sm"></i> <?php $Strings->get('category'); ?></th>
<th data-priority="2"><i class="fas fa-hashtag hidden-sm"></i> <?php $Strings->get('item count'); ?></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
</tr>
</tfoot>
</table>
</table>

View File

@ -28,43 +28,24 @@ if (!empty($VARS['id'])) {
header('Location: app.php?page=editcat');
}
}
?>
<form role="form" action="action.php" method="POST">
<div class="card border-green">
<h3 class="card-header text-green">
<?php
if ($editing) {
?>
<i class="fas fa-edit"></i> <?php $Strings->build("editing category", ['cat' => "<span id=\"name_title\">" . htmlspecialchars($catdata['catname']) . "</span>"]); ?>
<?php
} else {
?>
<i class="fas fa-edit"></i> <?php $Strings->get("Adding new category"); ?>
<?php
}
?>
</h3>
<div class="card-body">
<div class="form-group">
<label for="name"><i class="fas fa-archive"></i> <?php $Strings->get("name"); ?></label>
<input type="text" class="form-control" id="name" name="name" placeholder="Foo Bar" required="required" value="<?php echo htmlspecialchars($catdata['catname']); ?>" />
</div>
</div>
$form = new FormBuilder("", "fas fa-edit");
<input type="hidden" name="catid" value="<?php echo isset($VARS['id']) ? htmlspecialchars($VARS['id']) : ""; ?>" />
<input type="hidden" name="action" value="editcat" />
<input type="hidden" name="source" value="categories" />
if ($editing) {
$form->setTitle($Strings->build("editing category", ['cat' => "<span id=\"name_title\">" . htmlentities($catdata['catname']) . "</span>"], false));
} else {
$form->setTitle($Strings->get("Adding new category", false));
}
$form->addInput("name", htmlentities($catdata['catname']), "text", true, "name", null, $Strings->get("name", false), "fas fa-archive", 12);
<div class="card-footer d-flex">
<button type="submit" class="btn btn-success mr-auto"><i class="fas fa-save"></i> <?php $Strings->get("save"); ?></button>
<?php
if ($editing) {
?>
<a href="action.php?action=deletecat&source=categories&catid=<?php echo htmlspecialchars($VARS['id']); ?>" class="btn btn-danger ml-auto"><i class="fas fa-times"></i> <?php $Strings->get('delete'); ?></a>
<?php
}
?>
</div>
</div>
</form>
$form->addHiddenInput("catid", isset($VARS['id']) ? htmlspecialchars($VARS['id']) : "");
$form->addHiddenInput("action", "editcat");
$form->addHiddenInput("source", "categories");
$form->addButton($Strings->get("save", false), "fas fa-save", null, "submit");
if ($editing) {
$form->addButton($Strings->get("delete", false), "fas fa-times", "action.php?action=deletecat&source=categories&catid=" . htmlspecialchars($VARS['id']), "", null, null, "", "btn btn-danger ml-auto");
}
$form->generate();

View File

@ -84,7 +84,7 @@ if (!empty($VARS['id']) && $database->has('items', ['itemid' => $VARS['id']])) {
<div class="card-footer d-flex">
<?php
$source = "edititem";
if ($_GET['source'] === "item") {
if (!empty($_GET['source']) && $_GET['source'] === "item") {
$source = "item";
}
?>

View File

@ -164,7 +164,17 @@ if (!empty($VARS['id'])) {
<div class="col-12 col-md-3">
<div class="form-group">
<label for="qty"><i class="fas fa-hashtag"></i> <?php $Strings->get('quantity'); ?></label>
<input type="number" class="form-control" id="qty" name="qty" placeholder="1" value="<?php echo $itemdata['qty']; ?>" />
<?php
if ($SETTINGS['stock_management'] && $editing && !$cloning) {
?>
<input type="text" class="form-control" id="qty" name="qty" readonly value="<?php echo $itemdata['qty']; ?>" />
<?php
} else {
?>
<input type="number" class="form-control" id="qty" name="qty" placeholder="1" value="<?php echo $itemdata['qty']; ?>" />
<?php
}
?>
</div>
</div>
<div class="col-12 col-md-3">
@ -263,4 +273,4 @@ if (!empty($VARS['id'])) {
?>
</div>
</div>
</form>
</form>

View File

@ -34,58 +34,26 @@ if (!empty($VARS['id'])) {
header('Location: app.php?page=editloc');
}
}
?>
<form role="form" action="action.php" method="POST">
<div class="card border-green">
<h3 class="card-header text-green">
<?php
if ($editing) {
?>
<i class="fas fa-edit"></i> <?php $Strings->build("editing location", ['loc' => "<span id=\"name_title\">" . htmlspecialchars($locdata['locname']) . "</span>"]); ?>
<?php
} else {
?>
<i class="fas fa-edit"></i> <?php $Strings->get("Adding new location"); ?>
<?php
}
?>
</h3>
<div class="card-body">
<div class="row">
<div class="col-12 col-md-6">
<div class="form-group">
<label for="name"><i class="fas fa-map-marker"></i> <?php $Strings->get("name"); ?></label>
<input type="text" class="form-control" id="name" name="name" placeholder="<?php $Strings->get("placeholder location name"); ?>" required="required" value="<?php echo htmlspecialchars($locdata['locname']); ?>" />
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-group">
<label for="code"><i class="fas fa-barcode"></i> <?php $Strings->get("code"); ?></label>
<input type="text" class="form-control" id="code" name="code" placeholder="123456789" value="<?php echo htmlspecialchars($locdata['loccode']); ?>" />
</div>
</div>
</div>
$form = new FormBuilder("", "fas fa-edit");
<div class="form-group">
<label for="info"><i class="fas fa-info"></i> <?php $Strings->get("Description"); ?></label>
<textarea class="form-control" id="info" name="info"><?php echo htmlspecialchars($locdata['locinfo']); ?></textarea>
</div>
</div>
if ($editing) {
$form->setTitle($Strings->build("editing location", ['loc' => "<span id=\"name_title\">" . htmlentities($locdata['locname']) . "</span>"], false));
} else {
$form->setTitle($Strings->get("Adding new location", false));
}
$form->addInput("name", htmlentities($locdata['locname']), "text", true, "name", null, $Strings->get("name", false), "fas fa-map-marker", 6);
$form->addInput("code", htmlentities($locdata['loccode']), "text", false, "code", null, $Strings->get("code", false), "fas fa-barcode", 6);
$form->addInput("info", htmlentities($locdata['locinfo']), "textarea", false, "info", null, $Strings->get("Description", false), "fas fa-info", 12);
<input type="hidden" name="locid" value="<?php echo isset($VARS['id']) ? htmlspecialchars($VARS['id']) : ""; ?>" />
<input type="hidden" name="action" value="editloc" />
<input type="hidden" name="source" value="locations" />
$form->addHiddenInput("locid", isset($VARS['id']) ? htmlspecialchars($VARS['id']) : "");
$form->addHiddenInput("action", "editloc");
$form->addHiddenInput("source", "locations");
<div class="card-footer d-flex">
<button type="submit" class="btn btn-success mr-auto"><i class="fas fa-save"></i> <?php $Strings->get("save"); ?></button>
<?php
if ($editing) {
?>
<a href="action.php?action=deleteloc&source=locations&locid=<?php echo htmlspecialchars($VARS['id']); ?>" class="btn btn-danger ml-auto"><i class="fas fa-times"></i> <?php $Strings->get('delete'); ?></a>
<?php
}
?>
</div>
</div>
</form>
$form->addButton($Strings->get("save", false), "fas fa-save", null, "submit");
if ($editing) {
$form->addButton($Strings->get("delete", false), "fas fa-times", "action.php?action=deleteloc&source=locations&locid=" . htmlspecialchars($VARS['id']), "", null, null, "", "btn btn-danger ml-auto");
}
$form->generate();

View File

@ -16,6 +16,13 @@ redirectifnotloggedin();
<div class="form-group">
<label for="type"><?php $Strings->get("report type"); ?></label>
<select name="type" class="form-control" required>
<?php
if ($SETTINGS['stock_management']) {
?>
<option value="stock"><?php $Strings->get("Stock") ?></option>
<?php
}
?>
<option value="item"><?php $Strings->get("Items") ?></option>
<option value="category"><?php $Strings->get("Categories") ?></option>
<option value="location"><?php $Strings->get("Locations") ?></option>
@ -46,4 +53,4 @@ redirectifnotloggedin();
<button type="submit" class="btn btn-success ml-auto" id="genrptbtn"><i class="fas fa-download"></i> <?php $Strings->get("generate report"); ?></button>
</div>
</form>
</div>
</div>

View File

@ -169,5 +169,49 @@ $item = $database->get(
}
?>
</div>
<?php
if ($SETTINGS['stock_management']) {
?>
<hr />
<div class="row mt-4 mx-0">
<table id="stocktable" class="table table-bordered table-hover table-sm">
<thead>
<tr>
<th data-priority="1"><i class="fas fa-fw fa-calendar d-none d-md-inline"></i> <?php $Strings->get('date'); ?></th>
<th data-priority="2"><i class="fas fa-fw fa-hashtag d-none d-md-inline"></i> <?php $Strings->get('amount'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-sticky-note d-none d-md-inline"></i> <?php $Strings->get('description'); ?></th>
<th data-priority="4"><i class="fas fa-fw fa-user d-none d-md-inline"></i> <?php $Strings->get('changed by'); ?></th>
</tr>
</thead>
<tbody>
<?php
$stockentries = $database->select('stock', [
'timestamp',
'stock',
'text1',
'userid'
], [
'itemid' => $item['itemid']
]
);
foreach ($stockentries as $stockentry) {
$user = new User($stockentry['userid'])
?>
<tr>
<td><?php echo $stockentry['timestamp']; ?></td>
<td><?php echo $stockentry['stock']; ?></td>
<td><?php echo $stockentry['text1']; ?></td>
<td><?php echo $user->getName() . " (" . $user->getUsername() . ")"; ?></td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<?php
}
?>
</div>
</div>
</div>

View File

@ -24,7 +24,6 @@ redirectifnotloggedin();
<thead>
<tr>
<th data-priority="0"></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
<th data-priority="1"><i class="fas fa-fw fa-box d-none d-md-inline"></i> <?php $Strings->get('name'); ?></th>
<th data-priority="7"><i class="fas fa-fw fa-pallet d-none d-md-inline"></i> <?php $Strings->get('category'); ?></th>
<th data-priority="4"><i class="fas fa-fw fa-map-marker d-none d-md-inline"></i> <?php $Strings->get('location'); ?></th>
@ -33,6 +32,7 @@ redirectifnotloggedin();
<th data-priority="3"><i class="fas fa-fw fa-hashtag d-none d-md-inline"></i> <?php $Strings->get('qty'); ?></th>
<th data-priority="6"><i class="fas fa-fw fa-hashtag d-none d-md-inline"></i> <?php $Strings->get('want'); ?></th>
<th data-priority="8"><i class="fas fa-fw fa-user d-none d-md-inline"></i> <?php $Strings->get('assigned to'); ?></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
</tr>
</thead>
<tbody>
@ -40,7 +40,6 @@ redirectifnotloggedin();
<tfoot>
<tr>
<th data-priority="0"></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
<th data-priority="1"><i class="fas fa-fw fa-box d-none d-md-inline"></i> <?php $Strings->get('name'); ?></th>
<th data-priority="7"><i class="fas fa-fw fa-pallet d-none d-md-inline"></i> <?php $Strings->get('category'); ?></th>
<th data-priority="4"><i class="fas fa-fw fa-map-marker d-none d-md-inline"></i> <?php $Strings->get('location'); ?></th>
@ -49,6 +48,7 @@ redirectifnotloggedin();
<th data-priority="3"><i class="fas fa-fw fa-hashtag d-none d-md-inline"></i> <?php $Strings->get('qty'); ?></th>
<th data-priority="6"><i class="fas fa-fw fa-hashtag d-none d-md-inline"></i> <?php $Strings->get('want'); ?></th>
<th data-priority="8"><i class="fas fa-fw fa-user d-none d-md-inline"></i> <?php $Strings->get('assigned to'); ?></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
</tr>
</tfoot>
</table>

View File

@ -15,10 +15,10 @@ redirectifnotloggedin();
<thead>
<tr>
<th data-priority="0"></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
<th data-priority="1"><i class="fas fa-map-marker"></i> <?php $Strings->get('location'); ?></th>
<th data-priority="2"><i class="fas fa-barcode"></i> <?php $Strings->get('code'); ?></th>
<th data-priority="3"><i class="fas fa-hashtag"></i> <?php $Strings->get('item count'); ?></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
</tr>
</thead>
<tbody>
@ -33,12 +33,12 @@ redirectifnotloggedin();
?>
<tr>
<td></td>
<td>
<a class="btn btn-primary btn-sm" href="app.php?page=editloc&id=<?php echo $loc['locid']; ?>"><i class="fas fa-edit"></i> <?php $Strings->get("edit"); ?></a>
</td>
<td><?php echo $loc['locname']; ?></td>
<td><?php echo $loc['loccode']; ?></td>
<td><?php echo $itemcount; ?></td>
<td>
<a class="btn btn-primary btn-sm" href="app.php?page=editloc&id=<?php echo $loc['locid']; ?>"><i class="fas fa-edit"></i> <?php $Strings->get("edit"); ?></a>
</td>
</tr>
<?php
}
@ -47,10 +47,10 @@ redirectifnotloggedin();
<tfoot>
<tr>
<th data-priority="0"></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
<th data-priority="1"><i class="fas fa-map-marker"></i> <?php $Strings->get('location'); ?></th>
<th data-priority="2"><i class="fas fa-barcode"></i> <?php $Strings->get('code'); ?></th>
<th data-priority="3"><i class="fas fa-hashtag"></i> <?php $Strings->get('item count'); ?></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
</tr>
</tfoot>
</table>
</table>

77
pages/removestock.php Normal file
View File

@ -0,0 +1,77 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
require_once __DIR__ . '/../required.php';
redirectifnotloggedin();
if ($database->count("locations") == 0 || $database->count("categories") == 0) {
header('Location: app.php?page=items&msg=noloccat');
die();
}
$itemdata = [
'text1' => '',
'qty' => ''];
if (!empty($VARS['id'])) {
if ($database->has('items', ['itemid' => $VARS['id']])) {
$itemdata = $database->select(
'items', [
'name',
'qty',
], [
'itemid' => $VARS['id']
])[0];
} else {
// item id is invalid, redirect to a page that won't cause an error when pressing Save
header('Location: app.php?page=removestock');
die();
}
}
?>
<form role="form" action="action.php" method="POST">
<div class="card border-green">
<h3 class="card-header text-green">
<i class="fas fa-edit"></i> <?php $Strings->build("removing stock", ['item' => "<span id=\"name_title\">" . htmlspecialchars($itemdata['name']) . "</span>"]); ?>
</h3>
<div class="card-body">
<div class="row">
<div class="col-12 col-md-3">
<div class="form-group">
<label for="stock"><i class="fas fa-hashtag"></i> <?php $Strings->get('quantity'); ?></label>
<input type="number" min="1" class="form-control" id="stock" name="stock" required="required" placeholder="<?php echo $itemdata['qty']; ?>" />
</div>
</div>
</div>
<div class="row">
<div class="col-12 col-md-6">
<div class="form-group">
<label for="info1"><i class="fas fa-info"></i> <?php $Strings->get("Description"); ?></label>
<textarea class="form-control" id="info1" name="text1" required="required"></textarea>
</div>
</div>
</div>
</div>
<input type="hidden" name="itemid" value="<?php
echo htmlspecialchars($VARS['id']);
?>" />
<input type="hidden" name="action" value="removestock" />
<?php
if (isset($_GET['source']) && $_GET['source'] === "item") {
echo '<input type="hidden" name="source" value="item" />';
} else {
echo '<input type="hidden" name="source" value="items" />';
}
?>
<div class="card-footer d-flex">
<button type="submit" class="btn btn-success mr-1"><i class="fas fa-save"></i> <?php $Strings->get("save"); ?></button>
</div>
</div>
</form>

View File

@ -32,7 +32,6 @@ session_start(); // stick some cookies in it
// renew session cookie
setcookie(session_name(), session_id(), time() + $session_length, "/", false, false);
$captcha_server = (CAPTCHA_ENABLED === true ? preg_replace("/http(s)?:\/\//", "", CAPTCHA_SERVER) : "");
if ($_SESSION['mobile'] === TRUE) {
header("Content-Security-Policy: "
. "default-src 'self';"
@ -42,8 +41,8 @@ if ($_SESSION['mobile'] === TRUE) {
. "frame-src 'none'; "
. "font-src 'self'; "
. "connect-src *; "
. "style-src 'self' 'unsafe-inline' $captcha_server; "
. "script-src 'self' 'unsafe-inline' $captcha_server");
. "style-src 'self' 'unsafe-inline'; "
. "script-src 'self' 'unsafe-inline'");
} else {
header("Content-Security-Policy: "
. "default-src 'self';"
@ -53,8 +52,8 @@ if ($_SESSION['mobile'] === TRUE) {
. "frame-src 'none'; "
. "font-src 'self'; "
. "connect-src *; "
. "style-src 'self' 'nonce-$SECURE_NONCE' $captcha_server; "
. "script-src 'self' 'nonce-$SECURE_NONCE' $captcha_server");
. "style-src 'self' 'nonce-$SECURE_NONCE'; "
. "script-src 'self' 'nonce-$SECURE_NONCE'");
}
//
@ -69,7 +68,7 @@ foreach ($libs as $lib) {
require_once $lib;
}
$Strings = new Strings(LANGUAGE);
$Strings = new Strings($SETTINGS['language']);
/**
* Kill off the running process and spit out an error message
@ -93,7 +92,7 @@ function sendError($error) {
. "<p>" . htmlspecialchars($error) . "</p>");
}
date_default_timezone_set(TIMEZONE);
date_default_timezone_set($SETTINGS['timezone']);
// Database settings
// Also inits database and stuff
@ -102,12 +101,12 @@ use Medoo\Medoo;
$database;
try {
$database = new Medoo([
'database_type' => DB_TYPE,
'database_name' => DB_NAME,
'server' => DB_SERVER,
'username' => DB_USER,
'password' => DB_PASS,
'charset' => DB_CHARSET
'database_type' => $SETTINGS['database']['type'],
'database_name' => $SETTINGS['database']['name'],
'server' => $SETTINGS['database']['server'],
'username' => $SETTINGS['database']['user'],
'password' => $SETTINGS['database']['password'],
'charset' => $SETTINGS['database']['charset']
]);
} catch (Exception $ex) {
//header('HTTP/1.1 500 Internal Server Error');
@ -115,7 +114,7 @@ try {
}
if (!DEBUG) {
if (!$SETTINGS['debug']) {
error_reporting(0);
} else {
error_reporting(E_ALL);
@ -132,17 +131,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
define("GET", true);
}
/**
* Checks if a string or whatever is empty.
* @param $str The thingy to check
* @return boolean True if it's empty or whatever.
*/
function is_empty($str) {
return (is_null($str) || !isset($str) || $str == '');
}
function dieifnotloggedin() {
global $SETTINGS;
if ($_SESSION['loggedin'] != true) {
sendError("Session expired. Please log out and log in again.");
die();
@ -150,6 +140,13 @@ function dieifnotloggedin() {
if ((new User($_SESSION['uid']))->hasPermission("INV_VIEW") == FALSE) {
die("You don't have permission to be here.");
}
$user = new User($_SESSION['uid']);
foreach ($SETTINGS['permissions'] as $perm) {
if (!$user->hasPermission($perm)) {
session_destroy();
die("You don't have permission to be here.");
}
}
}
/**
@ -169,45 +166,18 @@ function checkDBError($specials = []) {
}
}
/*
* http://stackoverflow.com/a/20075147
*/
if (!function_exists('base_url')) {
function base_url($atRoot = FALSE, $atCore = FALSE, $parse = FALSE) {
if (isset($_SERVER['HTTP_HOST'])) {
$http = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off' ? 'https' : 'http';
$hostname = $_SERVER['HTTP_HOST'];
$dir = str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']);
$core = preg_split('@/@', str_replace($_SERVER['DOCUMENT_ROOT'], '', realpath(dirname(__FILE__))), NULL, PREG_SPLIT_NO_EMPTY);
$core = $core[0];
$tmplt = $atRoot ? ($atCore ? "%s://%s/%s/" : "%s://%s/") : ($atCore ? "%s://%s/%s/" : "%s://%s%s");
$end = $atRoot ? ($atCore ? $core : $hostname) : ($atCore ? $core : $dir);
$base_url = sprintf($tmplt, $http, $hostname, $end);
} else
$base_url = 'http://localhost/';
if ($parse) {
$base_url = parse_url($base_url);
if (isset($base_url['path']))
if ($base_url['path'] == '/')
$base_url['path'] = '';
}
return $base_url;
}
}
function redirectIfNotLoggedIn() {
global $SETTINGS;
if ($_SESSION['loggedin'] !== TRUE) {
header('Location: ./index.php');
header('Location: ' . $SETTINGS['url'] . '/index.php');
die();
}
if ((new User($_SESSION['uid']))->hasPermission("INV_VIEW") == FALSE) {
header('Location: ./index.php?permissionerror');
die("You don't have permission to be here.");
$user = new User($_SESSION['uid']);
foreach ($SETTINGS['permissions'] as $perm) {
if (!$user->hasPermission($perm)) {
session_destroy();
header('Location: ./index.php');
die("You don't have permission to be here.");
}
}
}

View File

@ -1,52 +1,62 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
// Whether to show debugging data in output.
// DO NOT SET TO TRUE IN PRODUCTION!!!
define("DEBUG", false);
// Settings for the app.
// Copy to settings.php and customize.
// Database connection settings
// See http://medoo.in/api/new for info
define("DB_TYPE", "mysql");
define("DB_NAME", "inventory");
define("DB_SERVER", "localhost");
define("DB_USER", "inventory");
define("DB_PASS", "");
define("DB_CHARSET", "utf8");
// Name of the app.
define("SITE_TITLE", "BinStack");
// URL of the AccountHub API endpoint
define("PORTAL_API", "http://localhost/accounthub/api.php");
// URL of the AccountHub home page
define("PORTAL_URL", "http://localhost/accounthub/home.php");
// AccountHub API Key
define("PORTAL_KEY", "123");
// For supported values, see http://php.net/manual/en/timezones.php
define("TIMEZONE", "America/Denver");
// Base URL for site links.
define('URL', '.');
// Folder for item images
// If in the webroot, verify that the contents of the folder are not accessible
// from a client (web browser).
define('FILE_UPLOAD_PATH', __DIR__ . '/images');
// Use Captcheck on login screen
// https://captcheck.netsyms.com
define("CAPTCHA_ENABLED", FALSE);
define('CAPTCHA_SERVER', 'https://captcheck.netsyms.com');
// See lang folder for language options
define('LANGUAGE', "en_us");
define("FOOTER_TEXT", "");
define("COPYRIGHT_NAME", "Netsyms Technologies");
$SETTINGS = [
// Whether to output debugging info like PHP notices, warnings,
// and stacktraces.
// Turning this on in production is a security risk and can sometimes break
// things, such as JSON output where extra content is not expected.
"debug" => false,
// Database connection settings
// See http://medoo.in/api/new for info
"database" => [
"type" => "mysql",
"name" => "binstack",
"server" => "localhost",
"user" => "app",
"password" => "",
"charset" => "utf8"
],
// Name of the app.
"site_title" => "BinStack",
// Settings for connecting to the AccountHub server.
"accounthub" => [
// URL for the API endpoint
"api" => "http://localhost/accounthub/api/",
// URL of the home page
"home" => "http://localhost/accounthub/home.php",
// API key
"key" => "123"
],
"file_upload_path" => __DIR__ . '/images',
// List of required user permissions to access this app.
"permissions" => [
"INV_VIEW"
],
// List of permissions required for API access. Remove to use the value of
// "permissions" instead.
"api_permissions" => [
],
// For supported values, see http://php.net/manual/en/timezones.php
"timezone" => "America/Denver",
// Language to use for localization. See langs folder to add a language.
"language" => "en",
// Shown in the footer of all the pages.
"footer_text" => "",
// Also shown in the footer, but with "Copyright <current_year>" in front.
"copyright" => "Netsyms Technologies",
// Base URL for building links relative to the location of the app.
// Only used when there's no good context for the path.
// The default is almost definitely fine.
"url" => ".",
// Enable stock management mode.
"stock_management" => false,
];

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@ -24,11 +24,11 @@ $('#cattable').DataTable({
orderable: false
},
{
targets: 1,
targets: 3,
orderable: false
}
],
order: [
[2, 'asc']
[1, 'asc']
]
});
});

File diff suppressed because one or more lines are too long

16
static/js/form.js Normal file
View File

@ -0,0 +1,16 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
$("#savebtn").click(function (event) {
var form = $("#sampleform");
if (form[0].checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
}
form.addClass("was-validated");
});

View File

@ -24,16 +24,16 @@ var itemtable = $('#itemtable').DataTable({
orderable: false
},
{
targets: 1,
targets: 8,
orderable: false
},
{
targets: 8,
targets: 9,
orderable: false
}
],
order: [
[2, 'asc']
[1, 'asc']
],
serverSide: true,
ajax: {
@ -49,7 +49,6 @@ var itemtable = $('#itemtable').DataTable({
json.items.forEach(function (row) {
json.data.push([
"",
"<span class='btn-group-vertical btn-group-sm'>" + row.viewbtn + " " + row.editbtn + "</span>",
row.name,
row.catname,
row.locname + " (" + row.loccode + ")",
@ -57,7 +56,8 @@ var itemtable = $('#itemtable').DataTable({
row.code2,
row.qty,
row.want,
row.username
row.username,
"<span class='btn-group btn-group-sm'>" + row.viewbtn + " " + row.editbtn + " " + row.addstockbtn + " " + row.removestockbtn + "</span>"
]);
});
return JSON.stringify(json);
@ -74,4 +74,4 @@ $(document).ready(function () {
$(searchInput).trigger("input");
$(searchInput).trigger("change");
}
});
});

View File

@ -24,11 +24,11 @@ $('#loctable').DataTable({
orderable: false
},
{
targets: 1,
targets: 4,
orderable: false
}
],
order: [
[2, 'asc']
[1, 'asc']
]
});
});