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 Licensed under the Mozilla Public License Version 2.0. Files without MPL header
assets with your own. comments, including third party code, may be under a different license.
The branding assets include:
* the application icon
* the Netsyms N punchcard logo
* the Netsyms for Business graph logo
If you are unsure if your usage is allowed, please contact us:
https://netsyms.com/contact
legal@netsyms.com
All other portions of this application,
unless otherwise noted (in comments, headers, etc), are licensed as follows:
Mozilla Public License Version 2.0 Mozilla Public License Version 2.0
================================== ==================================

View File

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

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 <?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 * 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/. */
@ -14,7 +13,7 @@ if ($_SESSION['loggedin'] != true) {
require_once __DIR__ . "/pages.php"; require_once __DIR__ . "/pages.php";
$pageid = "home"; $pageid = "home";
if (isset($_GET['page']) && !is_empty($_GET['page'])) { if (!empty($_GET['page'])) {
$pg = strtolower($_GET['page']); $pg = strtolower($_GET['page']);
$pg = preg_replace('/[^0-9a-z_]/', "", $pg); $pg = preg_replace('/[^0-9a-z_]/', "", $pg);
if (array_key_exists($pg, PAGES) && file_exists(__DIR__ . "/pages/" . $pg . ".php")) { 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 http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <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"> <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 <?php
// Alert messages // Alert messages
if (isset($_GET['msg']) && !is_empty($_GET['msg']) && array_key_exists($_GET['msg'], MESSAGES)) { if (!empty($_GET['msg'])) {
// optional string generation argument if (array_key_exists($_GET['msg'], MESSAGES)) {
if (!isset($_GET['arg']) || is_empty($_GET['arg'])) { // optional string generation argument
$alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false); 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 { } else {
$alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false); // We don't have a message for this, so just assume an error and escape stuff.
} $alertmsg = htmlentities($Strings->get($_GET['msg'], false));
$alerttype = MESSAGES[$_GET['msg']]['type']; $alerticon = "times";
$alerticon = "square-o"; $alerttype = "danger";
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;
} }
echo <<<END echo <<<END
<div class="row justify-content-center" id="msg-alert-box"> <div class="row justify-content-center" id="msg-alert-box">
@ -121,7 +127,7 @@ END;
</button> </button>
<a class="navbar-brand py-0 mr-auto" href="app.php"> <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" /> <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> </a>
<div class="collapse navbar-collapse py-0" id="navbar-collapse"> <div class="collapse navbar-collapse py-0" id="navbar-collapse">
@ -157,7 +163,7 @@ END;
</div> </div>
<div class="navbar-nav ml-auto py-0" id="navbar-right"> <div class="navbar-nav ml-auto py-0" id="navbar-right">
<span class="nav-item py-<?php echo $navbar_breakpoint; ?>-0"> <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> <i class="fas fa-user fa-fw"></i><span>&nbsp;<?php echo $_SESSION['realname'] ?></span>
</a> </a>
</span> </span>
@ -177,8 +183,8 @@ END;
?> ?>
</div> </div>
<div class="footer"> <div class="footer">
<?php echo FOOTER_TEXT; ?><br /> <?php echo $SETTINGS['footer_text']; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?> Copyright &copy; <?php echo date('Y'); ?> <?php echo $SETTINGS['copyright']; ?>
</div> </div>
</div> </div>
<script src="static/js/jquery-3.3.1.min.js"></script> <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 -- 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 -- Model: New Model Version: 1.0
-- MySQL Workbench Forward Engineering -- MySQL Workbench Forward Engineering
@ -55,14 +55,14 @@ CREATE TABLE IF NOT EXISTS `items` (
`price` DECIMAL(10,2) NULL, `price` DECIMAL(10,2) NULL,
PRIMARY KEY (`itemid`), PRIMARY KEY (`itemid`),
INDEX `fk_items_categories_idx` (`catid` ASC), 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), UNIQUE INDEX `itemid_UNIQUE` (`itemid` ASC),
CONSTRAINT `fk_items_categories` CONSTRAINT `fk_items_categories`
FOREIGN KEY (`catid`) FOREIGN KEY (`catid`)
REFERENCES `categories` (`catid`) REFERENCES `categories` (`catid`)
ON DELETE NO ACTION ON DELETE NO ACTION
ON UPDATE NO ACTION, ON UPDATE NO ACTION,
CONSTRAINT `fk_items_locations1` CONSTRAINT `fk_items_locations`
FOREIGN KEY (`locid`) FOREIGN KEY (`locid`)
REFERENCES `locations` (`locid`) REFERENCES `locations` (`locid`)
ON DELETE NO ACTION ON DELETE NO ACTION
@ -89,12 +89,12 @@ CREATE TABLE IF NOT EXISTS `permissions` (
`itemid` INT NOT NULL, `itemid` INT NOT NULL,
`canedit` TINYINT(1) NOT NULL DEFAULT 0, `canedit` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`userid`, `itemid`), PRIMARY KEY (`userid`, `itemid`),
INDEX `fk_permissions_items1_idx` (`itemid` ASC), INDEX `fk_permissions_items_idx` (`itemid` ASC),
CONSTRAINT `fk_permissions_items1` CONSTRAINT `fk_permissions_items`
FOREIGN KEY (`itemid`) FOREIGN KEY (`itemid`)
REFERENCES `items` (`itemid`) REFERENCES `items` (`itemid`)
ON DELETE NO ACTION ON DELETE CASCADE
ON UPDATE NO ACTION) ON UPDATE CASCADE)
ENGINE = InnoDB; ENGINE = InnoDB;
@ -120,12 +120,33 @@ CREATE TABLE IF NOT EXISTS `images` (
`primary` TINYINT(1) NOT NULL DEFAULT 0, `primary` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`imageid`, `itemid`), PRIMARY KEY (`imageid`, `itemid`),
UNIQUE INDEX `imageid_UNIQUE` (`imageid` ASC), UNIQUE INDEX `imageid_UNIQUE` (`imageid` ASC),
INDEX `fk_images_items1_idx` (`itemid` ASC), INDEX `fk_images_items_idx` (`itemid` ASC),
CONSTRAINT `fk_images_items1` CONSTRAINT `fk_images_items`
FOREIGN KEY (`itemid`) FOREIGN KEY (`itemid`)
REFERENCES `items` (`itemid`) REFERENCES `items` (`itemid`)
ON DELETE NO ACTION ON DELETE CASCADE
ON UPDATE NO ACTION) 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; ENGINE = InnoDB;

View File

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

264
index.php
View File

@ -1,175 +1,131 @@
<?php <?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 * 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"; require_once __DIR__ . "/required.php";
// if we're logged in, we don't need to be here. // if we're logged in, we don't need to be here.
if (!empty($_SESSION['loggedin']) && $_SESSION['loggedin'] === true && !isset($_GET['permissionerror'])) { if (!empty($_SESSION['loggedin']) && $_SESSION['loggedin'] === true && !isset($_GET['permissionerror'])) {
header('Location: app.php'); 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 */ <title><?php echo $SETTINGS['site_title']; ?></title>
$userpass_ok = false;
$multiauth = false; <link rel="icon" href="static/img/logo.svg">
if (Login::checkLoginServer()) {
if (empty($VARS['progress'])) { <link href="static/css/bootstrap.min.css" rel="stylesheet">
// Easy way to remove "undefined" warnings. <style nonce="<?php echo $SECURE_NONCE; ?>">
} else if ($VARS['progress'] == "1") { .display-5 {
if (!CAPTCHA_ENABLED || (CAPTCHA_ENABLED && Login::verifyCaptcha($VARS['captcheck_session_code'], $VARS['captcheck_selected_answer'], CAPTCHA_SERVER . "/api.php"))) { font-size: 2.5rem;
$autherror = ""; font-weight: 300;
$user = User::byUsername($VARS['username']); line-height: 1.2;
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);
} }
} else if ($VARS['progress'] == "2") {
$user = User::byUsername($VARS['username']); .banner-image {
if ($_SESSION['passok'] !== true) { max-height: 100px;
// stop logins using only username and authcode margin: 2em auto;
sendError("Password integrity check failed!"); border: 1px solid grey;
border-radius: 15%;
} }
if ($user->check2fa($VARS['authcode'])) { </style>
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">
<title><?php echo SITE_TITLE; ?></title> <div class="container mt-4">
<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="row justify-content-center"> <div class="row justify-content-center">
<div class="col-auto"> <div class="col-12 text-center">
<img class="banner-image" src="static/img/logo.svg" /> <img class="banner-image" src="./static/img/logo.svg" />
</div> </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) { <div class="col-12 text-center">
?> <h1 class="display-5 mb-4"><?php $Strings->get($title); ?></h1>
<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 /> </div>
<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="col-12 col-sm-8 col-lg-6">
<div class="captcheck_container" data-stylenonce="<?php echo $SECURE_NONCE; ?>"></div> <div class="card mt-4">
<br /> <div class="card-body">
<?php } ?> <a href="<?php echo $url; ?>" class="btn btn-primary btn-block"><?php $Strings->get($button); ?></a>
<input type="hidden" name="progress" value="1" /> </div>
<?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> </div>
</div> </div>
</div> </div>
<div class="footer">
<?php echo FOOTER_TEXT; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
</div>
</div> </div>
<script src="static/js/jquery-3.3.1.min.js"></script> <?php
<script src="static/js/bootstrap.bundle.min.js"></script> }
</body>
</html> 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", "save": "Save",
"delete": "Delete", "delete": "Delete",
"view": "View", "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", "sign out": "Sign out",
"settings": "Settings",
"options": "Options",
"404 error": "404 Error", "404 error": "404 Error",
"page not found": "Page not found.", "page not found": "Page not found.",
"invalid parameters": "Invalid request parameters.", "invalid parameters": "Invalid request parameters.",
"login server error": "The login server returned an error: {arg}", "login server error": "The login server returned an error: {arg}",
"login server user data error": "The login server refused to provide account information. Try again or contact technical support.", "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.", "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 permission": "You do not have permission to access this system.",
"no edit permission": "You do not have permission to modify records." "no edit permission": "You do not have permission to modify records."
} }

View File

@ -6,5 +6,7 @@
"Promoted": "Promoted", "Promoted": "Promoted",
"Promote": "Promote", "Promote": "Promote",
"Delete": "Delete", "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}", "cloning item": "Copying {oitem} <i class=\"fa fa-angle-right\"></i> {nitem}",
"itemid": "Item ID", "itemid": "Item ID",
"id": "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 1": "Code 1",
"code 2": "Code 2", "code 2": "Code 2",
"qty": "Qty", "qty": "Qty",
"want": "Need", "want": "Min",
"assigned to": "Assigned To", "assigned to": "Assigned to",
"quantity": "Quantity", "quantity": "Quantity",
"minwant": "Minimum On Hand", "minwant": "Minimum on hand",
"item count": "Item count", "item count": "Item count",
"Item cost": "Item cost", "Item cost": "Item cost",
"Sale price": "Sale price", "Sale price": "Sale price",
@ -18,5 +18,9 @@
"Notes": "Notes", "Notes": "Notes",
"Comments": "Comments", "Comments": "Comments",
"Cost": "Cost", "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.", "only showing understocked": "Only showing understocked items.",
"missing name": "You need to enter a name.", "missing name": "You need to enter a name.",
"use the dropdowns": "Whoops, you need to use the category and location autocomplete boxes.", "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", "choose an option": "Choose an option",
"csv file": "CSV text file", "csv file": "CSV text file",
"ods file": "ODS spreadsheet", "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" => [ "noloccat" => [
"string" => "make categories and locations", "string" => "make categories and locations",
"type" => "info" "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; return Login::LOGIN_OK;
} }
public static function verifyCaptcha(string $session, string $answer, string $url): bool {
$data = [
'session_id' => $session,
'answer_id' => $answer,
'action' => "verify"
];
$options = [
'http' => [
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($data)
]
];
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
$resp = json_decode($result, TRUE);
if (!$resp['result']) {
return false;
} else {
return true;
}
}
/** /**
* Check the login server API for sanity * Check the login server API for sanity
* @return boolean true if OK, else false * @return boolean true if OK, else false
*/ */
public static function checkLoginServer() { public static function checkLoginServer() {
try { try {
$client = new GuzzleHttp\Client(); $resp = AccountHubApi::get("ping");
$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);
if ($resp['status'] == "OK") { if ($resp['status'] == "OK") {
return true; return true;
} else { } else {
@ -107,19 +70,7 @@ class Login {
*/ */
function checkAPIKey($key) { function checkAPIKey($key) {
try { try {
$client = new GuzzleHttp\Client(); $resp = AccountHubApi::get("ping", null, true);
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => $key,
'action' => "ping"
]
]);
if ($response->getStatusCode() === 200) {
return true;
}
return false; return false;
} catch (Exception $e) { } catch (Exception $e) {
return false; return false;

View File

@ -32,27 +32,15 @@ class Notifications {
$timestamp = date("Y-m-d H:i:s", strtotime($timestamp)); $timestamp = date("Y-m-d H:i:s", strtotime($timestamp));
} }
$client = new GuzzleHttp\Client(); $resp = AccountHubApi::get("addnotification", [
'uid' => $user->getUID(),
$response = $client 'title' => $title,
->request('POST', PORTAL_API, [ 'content' => $content,
'form_params' => [ 'timestamp' => $timestamp,
'key' => PORTAL_KEY, 'url' => $url,
'action' => "addnotification", 'sensitive' => $sensitive
'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);
if ($resp['status'] == "OK") { if ($resp['status'] == "OK") {
return $resp['id'] * 1; return $resp['id'] * 1;
} else { } else {

View File

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

View File

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

View File

@ -32,32 +32,32 @@ if ($VARS['order'][0]['dir'] == 'asc') {
$sortby = "ASC"; $sortby = "ASC";
} }
switch ($VARS['order'][0]['column']) { switch ($VARS['order'][0]['column']) {
case 2: case 1:
$order = ["name" => $sortby]; $order = ["name" => $sortby];
break; break;
case 3: case 2:
$order = ["catname" => $sortby]; $order = ["catname" => $sortby];
break; break;
case 4: case 3:
$order = ["locname" => $sortby]; $order = ["locname" => $sortby];
break; break;
case 5: case 4:
$order = ["code1" => $sortby]; $order = ["code1" => $sortby];
break; break;
case 6: case 5:
$order = ["code2" => $sortby]; $order = ["code2" => $sortby];
break; break;
case 7: case 6:
$order = ["qty" => $sortby]; $order = ["qty" => $sortby];
break; break;
case 8: case 7:
$order = ["want" => $sortby]; $order = ["want" => $sortby];
break; break;
// Note: We're not going to sort by assigned user. It's too hard. Maybe later. // Note: We're not going to sort by assigned user. It's too hard. Maybe later.
} }
// search // search
if (!is_empty($VARS['search']['value'])) { if (!empty($VARS['search']['value'])) {
$filter = true; $filter = true;
$wherenolimit = []; $wherenolimit = [];
if ($showwant) { if ($showwant) {
@ -118,8 +118,17 @@ for ($i = 0; $i < count($items); $i++) {
$user = new User($_SESSION['uid']); $user = new User($_SESSION['uid']);
if ($user->hasPermission("INV_EDIT")) { 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>'; $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 { } else {
$items[$i]["editbtn"] = ''; $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>'; $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'])) { if (is_null($items[$i]['userid'])) {

View File

@ -75,7 +75,7 @@ function getItemReport($filter = []): Report {
$Strings->get("code 1", false), $Strings->get("code 1", false),
$Strings->get("code 2", false), $Strings->get("code 2", false),
$Strings->get("quantity", false), $Strings->get("quantity", false),
$Strings->get("want", false), $Strings->get("minwant", false),
$Strings->get("Cost", false), $Strings->get("Cost", false),
$Strings->get("Price", false), $Strings->get("Price", false),
$Strings->get("assigned to", false), $Strings->get("assigned to", false),
@ -151,6 +151,49 @@ function getLocationReport(): Report {
return $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 { function getReport($type): Report {
switch ($type) { switch ($type) {
case "item": case "item":
@ -165,6 +208,9 @@ function getReport($type): Report {
case "itemstock": case "itemstock":
return getItemReport(["AND" => ["qty[<]want", "want[>]" => 0]]); return getItemReport(["AND" => ["qty[<]want", "want[>]" => 0]]);
break; break;
case "stock":
return getStockReport();
break;
default: default:
return new Report("error", ["ERROR"], ["Invalid report type."]); return new Report("error", ["ERROR"], ["Invalid report type."]);
} }
@ -173,4 +219,4 @@ function getReport($type): Report {
function generateReport($type, $format) { function generateReport($type, $format) {
$report = getReport($type); $report = getReport($type);
$report->output($format); $report->output($format);
} }

View File

@ -8,10 +8,6 @@
* Mobile app API * Mobile app API
*/ */
// The name of the permission needed to log in.
// Set to null if you don't need it.
$access_permission = "INV_VIEW";
require __DIR__ . "/../required.php"; require __DIR__ . "/../required.php";
header('Content-Type: application/json'); header('Content-Type: application/json');
@ -23,21 +19,7 @@ if ($VARS['action'] == "ping") {
} }
function mobile_enabled() { function mobile_enabled() {
$client = new GuzzleHttp\Client(); $resp = AccountHubApi::get("mobileenabled");
$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);
if ($resp['status'] == "OK" && $resp['mobile'] === TRUE) { if ($resp['status'] == "OK" && $resp['mobile'] === TRUE) {
return true; return true;
} else { } else {
@ -46,26 +28,15 @@ function mobile_enabled() {
} }
function mobile_valid($username, $code) { function mobile_valid($username, $code) {
$client = new GuzzleHttp\Client(); try {
$resp = AccountHubApi::get("mobilevalid", ["code" => $code, "username" => $username], true);
$response = $client if ($resp['status'] == "OK" && $resp['valid'] === TRUE) {
->request('POST', PORTAL_API, [ return true;
'form_params' => [ } else {
'key' => PORTAL_KEY, return false;
"code" => $code, }
"username" => $username, } catch (Exception $ex) {
'action' => "mobilevalid"
]
]);
if ($response->getStatusCode() > 299) {
return false;
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK" && $resp['valid'] === TRUE) {
return true;
} else {
return false; return false;
} }
} }
@ -75,7 +46,7 @@ if (mobile_enabled() !== TRUE) {
} }
// Make sure we have a username and access key // 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); http_response_code(401);
die(json_encode(["status" => "ERROR", "msg" => "Missing username and/or access key."])); die(json_encode(["status" => "ERROR", "msg" => "Missing username and/or access key."]));
} }
@ -95,13 +66,14 @@ switch ($VARS['action']) {
if ($user->exists()) { if ($user->exists()) {
if ($user->getStatus()->getString() == "NORMAL") { if ($user->getStatus()->getString() == "NORMAL") {
if ($user->checkPassword($VARS['password'])) { if ($user->checkPassword($VARS['password'])) {
if (is_null($access_permission) || $user->hasPermission($access_permission)) { foreach ($SETTINGS['permissions'] as $perm) {
Session::start($user); if (!$user->hasPermission($perm)) {
$_SESSION['mobile'] = true; exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("no permission", false)]));
exit(json_encode(["status" => "OK"])); }
} else {
exit(json_encode(["status" => "ERROR", "msg" => lang("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" "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" => [ "categories" => [
"title" => "Categories", "title" => "Categories",
"navbar" => true, "navbar" => true,
@ -50,6 +37,19 @@ define("PAGES", [
"static/js/categories.js" "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" => [ "item" => [
"title" => "Item", "title" => "Item",
"navbar" => false "navbar" => false
@ -99,5 +99,27 @@ define("PAGES", [
], ],
"404" => [ "404" => [
"title" => "404 error" "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> <thead>
<tr> <tr>
<th data-priority="0"></th> <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="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="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> </tr>
</thead> </thead>
<tbody> <tbody>
@ -30,11 +30,11 @@ redirectifnotloggedin();
?> ?>
<tr> <tr>
<td></td> <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 $cat['catname']; ?></td>
<td><?php echo $itemcount; ?></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> </tr>
<?php <?php
} }
@ -43,9 +43,9 @@ redirectifnotloggedin();
<tfoot> <tfoot>
<tr> <tr>
<th data-priority="0"></th> <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="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="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> </tr>
</tfoot> </tfoot>
</table> </table>

View File

@ -28,43 +28,24 @@ if (!empty($VARS['id'])) {
header('Location: app.php?page=editcat'); header('Location: app.php?page=editcat');
} }
} }
?>
<form role="form" action="action.php" method="POST"> $form = new FormBuilder("", "fas fa-edit");
<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>
<input type="hidden" name="catid" value="<?php echo isset($VARS['id']) ? htmlspecialchars($VARS['id']) : ""; ?>" /> if ($editing) {
<input type="hidden" name="action" value="editcat" /> $form->setTitle($Strings->build("editing category", ['cat' => "<span id=\"name_title\">" . htmlentities($catdata['catname']) . "</span>"], false));
<input type="hidden" name="source" value="categories" /> } 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"> $form->addHiddenInput("catid", isset($VARS['id']) ? htmlspecialchars($VARS['id']) : "");
<button type="submit" class="btn btn-success mr-auto"><i class="fas fa-save"></i> <?php $Strings->get("save"); ?></button> $form->addHiddenInput("action", "editcat");
<?php $form->addHiddenInput("source", "categories");
if ($editing) {
?> $form->addButton($Strings->get("save", false), "fas fa-save", null, "submit");
<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 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");
?> }
</div>
</div> $form->generate();
</form>

View File

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

View File

@ -164,7 +164,17 @@ if (!empty($VARS['id'])) {
<div class="col-12 col-md-3"> <div class="col-12 col-md-3">
<div class="form-group"> <div class="form-group">
<label for="qty"><i class="fas fa-hashtag"></i> <?php $Strings->get('quantity'); ?></label> <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> </div>
<div class="col-12 col-md-3"> <div class="col-12 col-md-3">
@ -263,4 +273,4 @@ if (!empty($VARS['id'])) {
?> ?>
</div> </div>
</div> </div>
</form> </form>

View File

@ -34,58 +34,26 @@ if (!empty($VARS['id'])) {
header('Location: app.php?page=editloc'); header('Location: app.php?page=editloc');
} }
} }
?>
<form role="form" action="action.php" method="POST"> $form = new FormBuilder("", "fas fa-edit");
<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>
<div class="form-group"> if ($editing) {
<label for="info"><i class="fas fa-info"></i> <?php $Strings->get("Description"); ?></label> $form->setTitle($Strings->build("editing location", ['loc' => "<span id=\"name_title\">" . htmlentities($locdata['locname']) . "</span>"], false));
<textarea class="form-control" id="info" name="info"><?php echo htmlspecialchars($locdata['locinfo']); ?></textarea> } else {
</div> $form->setTitle($Strings->get("Adding new location", false));
</div> }
$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']) : ""; ?>" /> $form->addHiddenInput("locid", isset($VARS['id']) ? htmlspecialchars($VARS['id']) : "");
<input type="hidden" name="action" value="editloc" /> $form->addHiddenInput("action", "editloc");
<input type="hidden" name="source" value="locations" /> $form->addHiddenInput("source", "locations");
<div class="card-footer d-flex"> $form->addButton($Strings->get("save", false), "fas fa-save", null, "submit");
<button type="submit" class="btn btn-success mr-auto"><i class="fas fa-save"></i> <?php $Strings->get("save"); ?></button>
<?php if ($editing) {
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");
?> }
<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 $form->generate();
}
?>
</div>
</div>
</form>

View File

@ -16,6 +16,13 @@ redirectifnotloggedin();
<div class="form-group"> <div class="form-group">
<label for="type"><?php $Strings->get("report type"); ?></label> <label for="type"><?php $Strings->get("report type"); ?></label>
<select name="type" class="form-control" required> <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="item"><?php $Strings->get("Items") ?></option>
<option value="category"><?php $Strings->get("Categories") ?></option> <option value="category"><?php $Strings->get("Categories") ?></option>
<option value="location"><?php $Strings->get("Locations") ?></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> <button type="submit" class="btn btn-success ml-auto" id="genrptbtn"><i class="fas fa-download"></i> <?php $Strings->get("generate report"); ?></button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -169,5 +169,49 @@ $item = $database->get(
} }
?> ?>
</div> </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> </div>

View File

@ -24,7 +24,6 @@ redirectifnotloggedin();
<thead> <thead>
<tr> <tr>
<th data-priority="0"></th> <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="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="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> <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="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="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="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> </tr>
</thead> </thead>
<tbody> <tbody>
@ -40,7 +40,6 @@ redirectifnotloggedin();
<tfoot> <tfoot>
<tr> <tr>
<th data-priority="0"></th> <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="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="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> <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="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="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="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> </tr>
</tfoot> </tfoot>
</table> </table>

View File

@ -15,10 +15,10 @@ redirectifnotloggedin();
<thead> <thead>
<tr> <tr>
<th data-priority="0"></th> <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="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="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="3"><i class="fas fa-hashtag"></i> <?php $Strings->get('item count'); ?></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -33,12 +33,12 @@ redirectifnotloggedin();
?> ?>
<tr> <tr>
<td></td> <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['locname']; ?></td>
<td><?php echo $loc['loccode']; ?></td> <td><?php echo $loc['loccode']; ?></td>
<td><?php echo $itemcount; ?></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> </tr>
<?php <?php
} }
@ -47,10 +47,10 @@ redirectifnotloggedin();
<tfoot> <tfoot>
<tr> <tr>
<th data-priority="0"></th> <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="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="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="3"><i class="fas fa-hashtag"></i> <?php $Strings->get('item count'); ?></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
</tr> </tr>
</tfoot> </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 // renew session cookie
setcookie(session_name(), session_id(), time() + $session_length, "/", false, false); setcookie(session_name(), session_id(), time() + $session_length, "/", false, false);
$captcha_server = (CAPTCHA_ENABLED === true ? preg_replace("/http(s)?:\/\//", "", CAPTCHA_SERVER) : "");
if ($_SESSION['mobile'] === TRUE) { if ($_SESSION['mobile'] === TRUE) {
header("Content-Security-Policy: " header("Content-Security-Policy: "
. "default-src 'self';" . "default-src 'self';"
@ -42,8 +41,8 @@ if ($_SESSION['mobile'] === TRUE) {
. "frame-src 'none'; " . "frame-src 'none'; "
. "font-src 'self'; " . "font-src 'self'; "
. "connect-src *; " . "connect-src *; "
. "style-src 'self' 'unsafe-inline' $captcha_server; " . "style-src 'self' 'unsafe-inline'; "
. "script-src 'self' 'unsafe-inline' $captcha_server"); . "script-src 'self' 'unsafe-inline'");
} else { } else {
header("Content-Security-Policy: " header("Content-Security-Policy: "
. "default-src 'self';" . "default-src 'self';"
@ -53,8 +52,8 @@ if ($_SESSION['mobile'] === TRUE) {
. "frame-src 'none'; " . "frame-src 'none'; "
. "font-src 'self'; " . "font-src 'self'; "
. "connect-src *; " . "connect-src *; "
. "style-src 'self' 'nonce-$SECURE_NONCE' $captcha_server; " . "style-src 'self' 'nonce-$SECURE_NONCE'; "
. "script-src 'self' 'nonce-$SECURE_NONCE' $captcha_server"); . "script-src 'self' 'nonce-$SECURE_NONCE'");
} }
// //
@ -69,7 +68,7 @@ foreach ($libs as $lib) {
require_once $lib; require_once $lib;
} }
$Strings = new Strings(LANGUAGE); $Strings = new Strings($SETTINGS['language']);
/** /**
* Kill off the running process and spit out an error message * Kill off the running process and spit out an error message
@ -93,7 +92,7 @@ function sendError($error) {
. "<p>" . htmlspecialchars($error) . "</p>"); . "<p>" . htmlspecialchars($error) . "</p>");
} }
date_default_timezone_set(TIMEZONE); date_default_timezone_set($SETTINGS['timezone']);
// Database settings // Database settings
// Also inits database and stuff // Also inits database and stuff
@ -102,12 +101,12 @@ use Medoo\Medoo;
$database; $database;
try { try {
$database = new Medoo([ $database = new Medoo([
'database_type' => DB_TYPE, 'database_type' => $SETTINGS['database']['type'],
'database_name' => DB_NAME, 'database_name' => $SETTINGS['database']['name'],
'server' => DB_SERVER, 'server' => $SETTINGS['database']['server'],
'username' => DB_USER, 'username' => $SETTINGS['database']['user'],
'password' => DB_PASS, 'password' => $SETTINGS['database']['password'],
'charset' => DB_CHARSET 'charset' => $SETTINGS['database']['charset']
]); ]);
} catch (Exception $ex) { } catch (Exception $ex) {
//header('HTTP/1.1 500 Internal Server Error'); //header('HTTP/1.1 500 Internal Server Error');
@ -115,7 +114,7 @@ try {
} }
if (!DEBUG) { if (!$SETTINGS['debug']) {
error_reporting(0); error_reporting(0);
} else { } else {
error_reporting(E_ALL); error_reporting(E_ALL);
@ -132,17 +131,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
define("GET", true); 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() { function dieifnotloggedin() {
global $SETTINGS;
if ($_SESSION['loggedin'] != true) { if ($_SESSION['loggedin'] != true) {
sendError("Session expired. Please log out and log in again."); sendError("Session expired. Please log out and log in again.");
die(); die();
@ -150,6 +140,13 @@ function dieifnotloggedin() {
if ((new User($_SESSION['uid']))->hasPermission("INV_VIEW") == FALSE) { if ((new User($_SESSION['uid']))->hasPermission("INV_VIEW") == FALSE) {
die("You don't have permission to be here."); 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() { function redirectIfNotLoggedIn() {
global $SETTINGS;
if ($_SESSION['loggedin'] !== TRUE) { if ($_SESSION['loggedin'] !== TRUE) {
header('Location: ./index.php'); header('Location: ' . $SETTINGS['url'] . '/index.php');
die(); die();
} }
if ((new User($_SESSION['uid']))->hasPermission("INV_VIEW") == FALSE) { $user = new User($_SESSION['uid']);
header('Location: ./index.php?permissionerror'); foreach ($SETTINGS['permissions'] as $perm) {
die("You don't have permission to be here."); 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 <?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 * 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. // Settings for the app.
// DO NOT SET TO TRUE IN PRODUCTION!!! // Copy to settings.php and customize.
define("DEBUG", false);
// Database connection settings $SETTINGS = [
// See http://medoo.in/api/new for info // Whether to output debugging info like PHP notices, warnings,
define("DB_TYPE", "mysql"); // and stacktraces.
define("DB_NAME", "inventory"); // Turning this on in production is a security risk and can sometimes break
define("DB_SERVER", "localhost"); // things, such as JSON output where extra content is not expected.
define("DB_USER", "inventory"); "debug" => false,
define("DB_PASS", ""); // Database connection settings
define("DB_CHARSET", "utf8"); // See http://medoo.in/api/new for info
"database" => [
// Name of the app. "type" => "mysql",
define("SITE_TITLE", "BinStack"); "name" => "binstack",
"server" => "localhost",
"user" => "app",
// URL of the AccountHub API endpoint "password" => "",
define("PORTAL_API", "http://localhost/accounthub/api.php"); "charset" => "utf8"
// URL of the AccountHub home page ],
define("PORTAL_URL", "http://localhost/accounthub/home.php"); // Name of the app.
// AccountHub API Key "site_title" => "BinStack",
define("PORTAL_KEY", "123"); // Settings for connecting to the AccountHub server.
"accounthub" => [
// For supported values, see http://php.net/manual/en/timezones.php // URL for the API endpoint
define("TIMEZONE", "America/Denver"); "api" => "http://localhost/accounthub/api/",
// URL of the home page
// Base URL for site links. "home" => "http://localhost/accounthub/home.php",
define('URL', '.'); // API key
"key" => "123"
// Folder for item images ],
// If in the webroot, verify that the contents of the folder are not accessible "file_upload_path" => __DIR__ . '/images',
// from a client (web browser). // List of required user permissions to access this app.
define('FILE_UPLOAD_PATH', __DIR__ . '/images'); "permissions" => [
"INV_VIEW"
// Use Captcheck on login screen ],
// https://captcheck.netsyms.com // List of permissions required for API access. Remove to use the value of
define("CAPTCHA_ENABLED", FALSE); // "permissions" instead.
define('CAPTCHA_SERVER', 'https://captcheck.netsyms.com'); "api_permissions" => [
],
// See lang folder for language options // For supported values, see http://php.net/manual/en/timezones.php
define('LANGUAGE', "en_us"); "timezone" => "America/Denver",
// Language to use for localization. See langs folder to add a language.
"language" => "en",
define("FOOTER_TEXT", ""); // Shown in the footer of all the pages.
define("COPYRIGHT_NAME", "Netsyms Technologies"); "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 @@
/*! .svg-inline--fa,svg:not(:root).svg-inline--fa{overflow:visible}.svg-inline--fa{display:inline-block;font-size:inherit;height:1em;vertical-align:-.125em}.svg-inline--fa.fa-lg{vertical-align:-.225em}.svg-inline--fa.fa-w-1{width:.0625em}.svg-inline--fa.fa-w-2{width:.125em}.svg-inline--fa.fa-w-3{width:.1875em}.svg-inline--fa.fa-w-4{width:.25em}.svg-inline--fa.fa-w-5{width:.3125em}.svg-inline--fa.fa-w-6{width:.375em}.svg-inline--fa.fa-w-7{width:.4375em}.svg-inline--fa.fa-w-8{width:.5em}.svg-inline--fa.fa-w-9{width:.5625em}.svg-inline--fa.fa-w-10{width:.625em}.svg-inline--fa.fa-w-11{width:.6875em}.svg-inline--fa.fa-w-12{width:.75em}.svg-inline--fa.fa-w-13{width:.8125em}.svg-inline--fa.fa-w-14{width:.875em}.svg-inline--fa.fa-w-15{width:.9375em}.svg-inline--fa.fa-w-16{width:1em}.svg-inline--fa.fa-w-17{width:1.0625em}.svg-inline--fa.fa-w-18{width:1.125em}.svg-inline--fa.fa-w-19{width:1.1875em}.svg-inline--fa.fa-w-20{width:1.25em}.svg-inline--fa.fa-pull-left{margin-right:.3em;width:auto}.svg-inline--fa.fa-pull-right{margin-left:.3em;width:auto}.svg-inline--fa.fa-border{height:1.5em}.svg-inline--fa.fa-li{width:2em}.svg-inline--fa.fa-fw{width:1.25em}.fa-layers svg.svg-inline--fa{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.fa-layers{display:inline-block;height:1em;position:relative;text-align:center;vertical-align:-.125em;width:1em}.fa-layers svg.svg-inline--fa{transform-origin:center center}.fa-layers-counter,.fa-layers-text{display:inline-block;position:absolute;text-align:center}.fa-layers-text{left:50%;top:50%;transform:translate(-50%,-50%);transform-origin:center center}.fa-layers-counter{background-color:#ff253a;border-radius:1em;box-sizing:border-box;color:#fff;height:1.5em;line-height:1;max-width:5em;min-width:1.5em;overflow:hidden;padding:.25em;right:0;text-overflow:ellipsis;top:0;transform:scale(.25);transform-origin:top right}.fa-layers-bottom-right{bottom:0;right:0;top:auto;transform:scale(.25);transform-origin:bottom right}.fa-layers-bottom-left{bottom:0;left:0;right:auto;top:auto;transform:scale(.25);transform-origin:bottom left}.fa-layers-top-right{right:0;top:0;transform:scale(.25);transform-origin:top right}.fa-layers-top-left{left:0;right:auto;top:0;transform:scale(.25);transform-origin:top left}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{animation:fa-spin 2s infinite linear}.fa-pulse{animation:fa-spin 1s infinite steps(8)}@keyframes fa-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";transform:scaleX(-1)}.fa-flip-vertical{transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{display:inline-block;height:2em;position:relative;width:2.5em}.fa-stack-1x,.fa-stack-2x{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.svg-inline--fa.fa-stack-1x{height:1em;width:1.25em}.svg-inline--fa.fa-stack-2x{height:2em;width:2.5em}.fa-inverse{color:#fff}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}
* Font Awesome Free 5.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}

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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