Compare commits

...

203 Commits
v2.0 ... master

Author SHA1 Message Date
Skylar Ittner 356044fb2d Prevent registrations with same email 4 years ago
Skylar Ittner 368cb6bbbf Add text captcha to signup 4 years ago
Skylar Ittner 14a4fe7c46 Fix signup 4 years ago
Skylar Ittner 2749dfee32 Fix alert emails 4 years ago
Skylar Ittner 033bb50298 Fix alert emails, remove duplicate lang key 4 years ago
Skylar Ittner 7e1439dd3c Merge AppTemplate 4 years ago
Skylar Ittner 26b16ccbe6 Missed a spot 4 years ago
Skylar Ittner 3c6851e38e Update dependencies 4 years ago
Skylar Ittner bc0665b022 Update dependencies 4 years ago
Skylar Ittner 367062b76c Update FontAwesome version 4 years ago
Skylar Ittner 81ad5e653e Merge ../AppTemplate
# Conflicts:
#	README.md
#	composer.lock
#	nbproject/project.xml
4 years ago
Skylar Ittner a454dac629 Fix warning message (#17) 4 years ago
Skylar Ittner 589364201c Update project title 4 years ago
Skylar Ittner b36b4080f5 Update dependencies (fix CVE-2018-19296) 4 years ago
Skylar Ittner ac1ad47aba Add redirect parameters to signup link 4 years ago
Skylar Ittner bfae187a59 Add API call for adding app password 5 years ago
Skylar Ittner 88cb51f1bb Upgrade Medoo: 1.5 to 1.6 5 years ago
Skylar Ittner f27812997f Use FontAwesome CSS+webfont instead of JS, upgrade 5.7.2 to 5.8.1 5 years ago
Skylar Ittner 42460d2165 Adjust User.lib.php 5 years ago
Skylar Ittner 59136bd8eb Merge ../BusinessAppTemplate
# Conflicts:
#	README.md
#	action.php
#	api/functions.php
#	lib/User.lib.php
#	mobile/index.php
#	pages/form.php
#	settings.template.php
#	static/js/form.js
5 years ago
Skylar Ittner 53e158b553 Update FontAwesome 5.6.0 -> 5.7.2 5 years ago
Skylar Ittner 474047ab34 Update Bootstrap 4.1.3 -> 4.3.1 5 years ago
Skylar Ittner 922ea55cdb Fix #16: add back button to login flow that redirects to username entry 5 years ago
Skylar Ittner c97e058786 API: Check for user permission 5 years ago
Skylar Ittner 26a662c399 Add `addTextInput` and `addSelect` methods that are simpler than `addInput` 5 years ago
Skylar Ittner 289aaeaa9f Minor text changes 5 years ago
Skylar Ittner df79def142 Update sync settings card text 5 years ago
Skylar Ittner de12184bf4 Update security card text 5 years ago
Skylar Ittner 61acc9710b Fix mistake in upgrade SQL 5 years ago
Skylar Ittner 3ca062d995 Enforce app passwords in API for users with two-factor enabled 5 years ago
Skylar Ittner 04702f6090 Check for apppass option in login api 5 years ago
Skylar Ittner 22fb97d0c4 Add app passwords (close #15) 5 years ago
Skylar Ittner 99f2e07f63 Add API key types 5 years ago
Skylar Ittner 29fb7feb85 Add API key type column to `apikeys`, update database schema 5 years ago
Skylar Ittner 7d30251cd6 Add CORS header to API 5 years ago
Skylar Ittner 7173a50c36 Add textarea to FormBuilder 5 years ago
Skylar Ittner e66280e07a FormBuilder: add d-flex to footer 5 years ago
Skylar Ittner 3ed75822a1 Update license and readme 5 years ago
Skylar Ittner 7531dc362d Whoops 5 years ago
Skylar Ittner b250908663 Add more permissions checks 5 years ago
Skylar Ittner 5f7d45e812 Add ToS agree checkbox 5 years ago
Skylar Ittner 0fd1aa2b54 Merge ../BusinessAppTemplate 5 years ago
Skylar Ittner 892102528b Strip tags from aria-label 5 years ago
Skylar Ittner a89c663ca9 Merge BusinessAppTemplate 5 years ago
Skylar Ittner 69c634ea99 Add checkbox to form builder 5 years ago
Skylar Ittner a514e66969 Add create account button to login page 5 years ago
Skylar Ittner 5b98d3e00a i18n++ 5 years ago
Skylar Ittner d853082cdb Better button icon 5 years ago
Skylar Ittner c7aad627ac Add user self-registration option 5 years ago
Skylar Ittner 6ceeeaa087 Add support for regex matching on API vars 5 years ago
Skylar Ittner 4600c87787 Add logging to new login flow 5 years ago
Skylar Ittner f1c36fdeb1 Add getRequestUser() function 5 years ago
Skylar Ittner d36b340692 Make API work with user/pass combo 5 years ago
Skylar Ittner 67388884f0 Move LoginKeys.lib.php to LoginKey.lib.php so the class matches the file 5 years ago
Skylar Ittner 223a431e8b Merge BusinessAppTemplate 5 years ago
Skylar Ittner d7ca7125ce Nicer access denied message 5 years ago
Skylar Ittner 1729b842ba Add permission check during login 5 years ago
Skylar Ittner 4d2b78bdba Remove unused strings 5 years ago
Skylar Ittner 106e697fc3 Remove captcha-related code, since login is done by AccountHub now 5 years ago
Skylar Ittner e0802f582b Remove unneeded index.css 5 years ago
Skylar Ittner 93098309cb Fix 5 years ago
Skylar Ittner 6d4144c78d Clear login session variables after success to prevent skipping username entry 5 years ago
Skylar Ittner 51de8283b8 Fix index.php not redirecting to app.php when already logged in 5 years ago
Skylar Ittner 016c71d30d Fix index.php not redirecting to app.php when already logged in 5 years ago
Skylar Ittner 2698fc794e Fix issue with database script and settings template 5 years ago
Skylar Ittner ba1369d842 Add app icon to login flow 5 years ago
Skylar Ittner 2836a05f90 Add app icon to login flow 5 years ago
Skylar Ittner 27502ed710 Fix unclosed div tag 5 years ago
Skylar Ittner 3d3e975519 Fix logout redirect 5 years ago
Skylar Ittner a559901ac0 Redirect to AccountHub for user login 5 years ago
Skylar Ittner 74971a4592 Add new more flexible login flow 5 years ago
Skylar Ittner 16be9438b9 Minor text fixes 5 years ago
Skylar Ittner bb5639c447 Merge BusinessAppTemplate (new settings.php format)
# Conflicts:
#	api.php
#	api/apisettings.php
#	api/index.php
#	app.php
#	index.php
#	langs/en/titles.json
#	lib/Login.lib.php
#	lib/Notifications.lib.php
#	lib/User.lib.php
#	mobile/index.php
#	required.php
#	settings.template.php
5 years ago
Skylar Ittner 3f32258ba0 Fix bug 5 years ago
Skylar Ittner 129efd13c7 Add documentation comments to settings 5 years ago
Skylar Ittner c179ed7ebb Make settings.php an array, not a bunch of defines 5 years ago
Skylar Ittner f1a85f47fd Add comment 5 years ago
Skylar Ittner 61d660be69 Add FormBuilder 5 years ago
Skylar Ittner 20a6c6f143 Fix bug 5 years ago
Skylar Ittner 5b7ab65946 Make better API system, use new AccountHub API 5 years ago
Skylar Ittner fb25c4395a Make better API system 5 years ago
Skylar Ittner 13b60de915 Remove is_empty() 5 years ago
Skylar Ittner 0e094809fa Cleanup some technical debt 5 years ago
Skylar Ittner ca179b89ea Add option to hide Station PIN settings, update branding 5 years ago
Skylar Ittner 32cd18933d Update FontAwesome from 5.3.1 to 5.6.0 5 years ago
Skylar Ittner 4f1b81ff4b Deprecate is_empty() 5 years ago
Skylar Ittner cb3c8aaf2d Support undefined messages 5 years ago
Skylar Ittner f1f682780c Merge BusinessAppTemplate
# Conflicts:
#	.gitignore
#	README.md
#	action.php
#	api.php
#	app.php
#	composer.json
#	composer.lock
#	index.php
#	langs/en/core.json
#	langs/en/titles.json
#	langs/messages.php
#	lib/Exceptions.lib.php
#	lib/Login.lib.php
#	lib/Notifications.lib.php
#	lib/Strings.lib.php
#	lib/User.lib.php
#	mobile/index.php
#	nbproject/project.properties
#	nbproject/project.xml
#	pages.php
#	pages/home.php
#	required.php
#	settings.template.php
#	static/css/bootstrap.min.css
#	static/img/logo.svg
#	static/js/app.js
5 years ago
Skylar Ittner b55eaea821 Improve noscript UX 5 years ago
Skylar Ittner 1d81bfb83d Remove app bar 5 years ago
Skylar Ittner ec44a6740f Fix "language key ... is defined more than once" warning 5 years ago
Skylar Ittner e714286e5a Close #13 6 years ago
Skylar Ittner bcc41b887d Update database.sql 6 years ago
Skylar Ittner eee5af3081 Add generatesynccode mobile API (Business/MobileApp/issues/15) 6 years ago
Skylar Ittner 47539de2d7 Fix bootstrap font URL 6 years ago
Skylar Ittner 12aea4a2e2 Update Bootstrap (https://github.com/thomaspark/bootswatch/issues/861) 6 years ago
Skylar Ittner 4c135d6e59 Update composer, adjust margin 6 years ago
Skylar Ittner 34f49bfd01 Update README.md 6 years ago
Skylar Ittner d4621de80f Update README.md 6 years ago
Skylar Ittner 39ccaa2f2d Add RSS/ATOM notification feeds, close #12 6 years ago
Skylar Ittner f43f986e25 Adjust cards 6 years ago
Skylar Ittner c36c365a1b Update Bootstrap and FontAwesome 6 years ago
Skylar Ittner 8dd7ee6005 Update Bootstrap and FontAwesome 6 years ago
Skylar Ittner 80d0a017ed Update Bootstrap to 4.1.3 6 years ago
Skylar Ittner a17f51b72d Update FontAwesome 5.1.0 to 5.3.1 6 years ago
Skylar Ittner 1271317eb9 Rewrite to use classes, aligning with AccountHub 2.0 6 years ago
Skylar Ittner 2caec48e4c Add mobile notification API calls 6 years ago
Skylar Ittner 963fbfbf00 Upgrade FontAwesome to 5.1.0 6 years ago
Skylar Ittner 10575f6f59 Fix another visual bug 6 years ago
Skylar Ittner 2f9eccf931 Fix bug where clicking close on msg alert didn't remove progress bar 6 years ago
Skylar Ittner 769d24b4b7 Improve alert fadeout 6 years ago
Skylar Ittner 66aa3d6fdc Make msg alert time out with smooth visual effects 6 years ago
Skylar Ittner ee0c0f65e3 Fix PHP variable warnings 6 years ago
Skylar Ittner 66fa86e04e Change string 6 years ago
Skylar Ittner dafc3b76ea Add message if user is kicked out of application for lack of permissions 6 years ago
Skylar Ittner 8e65d4c98d Use bundled Roboto font 6 years ago
Skylar Ittner 9dd9f9297c Fix a few undefined variable notices 6 years ago
Skylar Ittner 41c8b6c16b Fix #2 6 years ago
Skylar Ittner be34857d71 Change session ID on successful login, make sessions last at least 2 hours 6 years ago
Skylar Ittner 58a991cbd0 Update FontAwesome to 5.0.13 6 years ago
Skylar Ittner 023480bf88 Use SVG logo for login page 6 years ago
Skylar Ittner 4bab466169 Fix #1 6 years ago
Skylar Ittner d4070b36b9 Update FontAwesome 5.0.10 => 5.0.12, update Materia Bootstrap 6 years ago
Skylar Ittner 2d98c68efd Remove unused old arrow icons 6 years ago
Skylar Ittner 2461fec102 Update FontAwesome to 5.0.10 6 years ago
Skylar Ittner fa6924eb08 Update bootstrap.min.css with upstream bugfix from Bootswatch (https://github.com/thomaspark/bootswatch/issues/819) 6 years ago
Skylar Ittner a0d2293a3d Use captcheck.dist.js 6 years ago
Skylar Ittner a9eb59c936 Replace reCAPTCHA with Captcheck 6 years ago
Skylar Ittner 64c3d47c32 Add Roboto font files 6 years ago
Skylar Ittner 6a7ea5eeb7 Upgrade to Bootstrap 4.1 6 years ago
Skylar Ittner ca6e1f2c5a Update README 6 years ago
Skylar Ittner 038a712b88 Update README 6 years ago
Skylar Ittner 64add57446 Add MPL header 6 years ago
Skylar Ittner d10c6214a6 Switch to Mozilla Public License 2.0 for code consistency 6 years ago
Skylar Ittner 2a0d5bc92b Add some more preload headers 6 years ago
Skylar Ittner d193f3df4a Update license 6 years ago
Skylar Ittner 814c0dbc0f Fix URL 6 years ago
Skylar Ittner 386615976e Remove test page 6 years ago
Skylar Ittner 35e531a56b Add link preload headers 6 years ago
Skylar Ittner 644e5c2e37 Add <link rel="icon"> 6 years ago
Skylar Ittner 0691dd51f1 Remove unused CSS 6 years ago
Skylar Ittner 8441008219 Remove unneeded font files 6 years ago
Skylar Ittner e155ebe165 Update material-color submodule 6 years ago
Skylar Ittner c7c7e4e4ea Hide brand-icon on smaller screens 6 years ago
Skylar Ittner ee47026c0e Adjust alerts 6 years ago
Skylar Ittner eefa7ab00f Improve mobile app compatibility 6 years ago
Skylar Ittner 121a49e9e0 Upgrade to Bootstrap 4.0, FontAwesome 5.0, and jQuery 3.3.1 6 years ago
Skylar Ittner ce8e0fb4e3 Improve isManagerOf() error handling to prevent possible security bug 6 years ago
Skylar Ittner abb306a36e Collapse navbar and iconify right-side navbar items on small screens 6 years ago
Skylar Ittner 2e3cfb9546 Iconify right-side navbar items on small screens 6 years ago
Skylar Ittner d54ebed189 Update dependencies 6 years ago
Skylar Ittner 7c44b18854 Add checkAPIKey($key) function to login.php 6 years ago
Skylar Ittner b505a74502 s/LICENSE_TEXT/FOOTER_TEXT 6 years ago
Skylar Ittner 37789df696 Trigger warning with missing lang strings 6 years ago
Skylar Ittner 5277c5e0fb `composer update` 7 years ago
Skylar Ittner 5dae7bc168 Refactor and enforce Content-Security-Policy 7 years ago
Skylar Ittner a04207da62 Update cookie settings 7 years ago
Skylar Ittner c0a93fb666 Convert material-color CSS to submodule 7 years ago
Skylar Ittner 496b213a88 Fix Business/CommonBugs#1 (expired session logout error) 7 years ago
Skylar Ittner bf76d3733c Optimize icons 7 years ago
Skylar Ittner 8d2ac32419 Update settings template for Portal->AccountHub branding 7 years ago
Skylar Ittner d229aee8d0 Nothing 7 years ago
Skylar Ittner 5588ee494d Improve mobile integration, add autocorrect etc. flags to login fields 7 years ago
Skylar Ittner 2770e96a8a Move menu button around on mobile 7 years ago
Skylar Ittner 3d01bf8feb Remove debug data 7 years ago
Skylar Ittner 0405f695f3 Add mobile API 7 years ago
Skylar Ittner 4ad42bfe48 Add sendLoginAlertEmail() code 7 years ago
Skylar Ittner 0c85643847 Update composer packages 7 years ago
Skylar Ittner d918af5d65 Add account_has_permission function 7 years ago
Skylar Ittner 89c6c720f1 Add material-color to index.php 7 years ago
Skylar Ittner 542bb80d85 Update dependencies 7 years ago
Skylar Ittner 2284117cfc add JS to remove &msg=xyz from URL, update material-color to v1.2 7 years ago
Skylar Ittner b9f385d6f0 Update material-color.css to version 1.1 7 years ago
Skylar Ittner 3ab33d19f6 Add material-color.css 7 years ago
Skylar Ittner bdcb7e263d Add Material Design arrow icon to replace app icon when not on the homepage 7 years ago
Skylar Ittner 47f51420fa Add border on app icon when shown large 7 years ago
Skylar Ittner 0b811feccb Add api.php 7 years ago
Skylar Ittner e8c9cd56e2 Remove unused config variables, remove uncompressed CSS 7 years ago
Skylar Ittner d6df9d582c User name in navbar now links to Portal home 7 years ago
Skylar Ittner 455a199d78 Update README 7 years ago
Skylar Ittner ab53d719da Update README 7 years ago
Skylar Ittner 2f31066a0c Add lib/userinfo.php 7 years ago
Skylar Ittner ea71b78169 Update README 7 years ago
Skylar Ittner 427390cbc4 Add a bit of cleanup and comments 7 years ago
Skylar Ittner f14393a8a4 Add readme link to Portal API docs 7 years ago
Skylar Ittner bae6d1ac17 Add renew session cookie on page load 7 years ago
Skylar Ittner de4dcc37bc Add uid_exists($uid), better login error messages 7 years ago
Skylar Ittner eaeb8806a1 Add navbar color theme CSS options 7 years ago
Skylar Ittner 8b9407c274 Pages can now define extra stylesheet and script URLs 7 years ago
Skylar Ittner 501f127f04 Disable autocomplete on auth code input 7 years ago
Skylar Ittner dcd495f4e4 Add reCAPTCHA support, fix bug that allowed logins with only a username and 2fa code 7 years ago
Skylar Ittner c6941c7bd3 Remove unused variable reference 7 years ago
Skylar Ittner 17c587d3b2 Update readme 7 years ago
Skylar Ittner ef6bddddeb Update readme 7 years ago
Skylar Ittner 591b5e6ff1 Update readme 7 years ago
Skylar Ittner 8afe41070b Update readme 7 years ago
Skylar Ittner 292ce29b31 Update readme 7 years ago
Skylar Ittner 71347c33f1 Update readme 7 years ago
Skylar Ittner 35df787547 Add LICENSE 7 years ago
Skylar Ittner ba5ba051e9 Add readme 7 years ago
Skylar Ittner e28d3a93ac Clean up clutter and unneeded code 7 years ago
Skylar Ittner 3110011596 Add Portal API integration, add icon/style settings, add navbar and icon
options to PAGES
7 years ago
Skylar Ittner 16cbf2a5f1 Create template based on SSO code 7 years ago

2
.gitignore vendored

@ -4,4 +4,4 @@
/nbproject/private
*.sync-conflict*
test*
/conf/
/conf/

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

@ -23,11 +23,11 @@ dieifnotloggedin();
function returnToSender($msg, $arg = "") {
global $VARS;
if ($arg == "") {
header("Location: app.php?page=" . urlencode($VARS['source']) . "&msg=$msg");
} else {
header("Location: app.php?page=" . urlencode($VARS['source']) . "&msg=$msg&arg=" . urlencode($arg));
$header = "Location: app.php?page=" . urlencode($VARS['source']) . "&msg=$msg";
if ($arg != "") {
$header .= "&arg=$arg";
}
header($header);
die();
}
@ -35,7 +35,7 @@ switch ($VARS['action']) {
case "signout":
Log::insert(LogType::LOGOUT, $_SESSION['uid']);
session_destroy();
header('Location: index.php');
header('Location: index.php?logout=1');
die("Logged out.");
case "chpasswd":
engageRateLimit();
@ -70,7 +70,7 @@ switch ($VARS['action']) {
returnToSender("new_pin_mismatch");
break;
case "add2fa":
if (is_empty($VARS['secret'])) {
if (empty($VARS['secret'])) {
returnToSender("invalid_parameters");
}
$user = new User($_SESSION['uid']);
@ -113,4 +113,8 @@ switch ($VARS['action']) {
returnToSender("invalid_parameters#notifications");
}
break;
}
case "resetfeedkey":
$database->delete('userkeys', ['AND' => ['uid' => $_SESSION['uid'], 'typeid' => 1]]);
returnToSender("feed_key_reset");
break;
}

@ -4,439 +4,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Simple JSON API to allow other apps to access accounts in this system.
*
* 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");
if (empty($VARS['key'])) {
die("\"403 Unauthorized\"");
} else {
$key = $VARS['key'];
if ($database->has('apikeys', ['key' => $key]) !== TRUE) {
engageRateLimit();
http_response_code(403);
Log::insert(LogType::API_BAD_KEY, null, "Key: " . $key);
die("\"403 Unauthorized\"");
}
}
/**
* 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;
}
if (empty($VARS['action'])) {
http_response_code(404);
die(json_encode("No action specified."));
}
switch ($VARS['action']) {
case "ping":
exit(json_encode(["status" => "OK"]));
break;
case "auth":
$user = User::byUsername($VARS['username']);
if ($user->checkPassword($VARS['password'])) {
Log::insert(LogType::API_AUTH_OK, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "OK", "msg" => $Strings->get("login successful", false)]));
} else {
Log::insert(LogType::API_AUTH_FAILED, $user->getUID(), "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
if ($user->exists()) {
switch ($user->getStatus()->get()) {
case AccountStatus::LOCKED_OR_DISABLED:
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account locked", false)]));
case AccountStatus::TERMINATED:
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account terminated", false)]));
case AccountStatus::CHANGE_PASSWORD:
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("password expired", false)]));
case AccountStatus::NORMAL:
break;
default:
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account state error", false)]));
}
}
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
}
break;
case "userinfo":
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid']) && is_numeric($VARS['uid'])) {
$user = new User($VARS['uid']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
if ($user->exists()) {
$data = $database->get("accounts", ["uid", "username", "realname (name)", "email", "phone" => ["phone1 (1)", "phone2 (2)"], 'pin'], ["uid" => $user->getUID()]);
$data['pin'] = (is_null($data['pin']) || $data['pin'] == "" ? false : true);
exit(json_encode(["status" => "OK", "data" => $data]));
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
}
break;
case "userexists":
if (!empty($VARS['uid']) && is_numeric($VARS['uid'])) {
$user = new User($VARS['uid']);
} else if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
exit(json_encode(["status" => "OK", "exists" => $user->exists()]));
break;
case "hastotp":
exit(json_encode(["status" => "OK", "otp" => User::byUsername($VARS['username'])->has2fa()]));
break;
case "verifytotp":
$user = User::byUsername($VARS['username']);
if ($user->check2fa($VARS['code'])) {
exit(json_encode(["status" => "OK", "valid" => true]));
} else {
Log::insert(LogType::API_BAD_2FA, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("2fa incorrect", false), "valid" => false]));
}
break;
case "acctstatus":
exit(json_encode(["status" => "OK", "account" => User::byUsername($VARS['username'])->getStatus()->getString()]));
case "login":
// simulate a login, checking account status and alerts
engageRateLimit();
$user = User::byUsername($VARS['username']);
if ($user->checkPassword($VARS['password'])) {
switch ($user->getStatus()->getString()) {
case "LOCKED_OR_DISABLED":
Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account locked", false)]));
case "TERMINATED":
Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account terminated", false)]));
case "CHANGE_PASSWORD":
Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("password expired", false)]));
case "NORMAL":
Log::insert(LogType::API_LOGIN_OK, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "OK"]));
case "ALERT_ON_ACCESS":
$user->sendAlertEmail();
Log::insert(LogType::API_LOGIN_OK, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "OK", "alert" => true]));
default:
Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account state error", false)]));
}
} else {
Log::insert(LogType::API_LOGIN_FAILED, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
}
break;
case "ismanagerof":
if ($VARS['uid'] == "1") {
$manager = new User($VARS['manager']);
$employee = new User($VARS['employee']);
} else {
$manager = User::byUsername($VARS['manager']);
$employee = User::byUsername($VARS['employee']);
}
if (!$manager->exists()) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false), "user" => $VARS['manager']]));
}
if (!$employee->exists()) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false), "user" => $VARS['employee']]));
}
if ($database->has('managers', ['AND' => ['managerid' => $manager->getUID(), 'employeeid' => $employee->getUID()]])) {
exit(json_encode(["status" => "OK", "managerof" => true]));
} else {
exit(json_encode(["status" => "OK", "managerof" => false]));
}
break;
case "getmanaged":
if (!empty($VARS['uid'])) {
$manager = new User($VARS['uid']);
} else if (!empty($VARS['username'])) {
$manager = User::byUsername($VARS['username']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
if (!$manager->exists()) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
}
if ($VARS['get'] == "username") {
$managed = $database->select('managers', ['[>]accounts' => ['employeeid' => 'uid']], 'username', ['managerid' => $manager->getUID()]);
} else {
$managed = $database->select('managers', 'employeeid', ['managerid' => $manager->getUID()]);
}
exit(json_encode(["status" => "OK", "employees" => $managed]));
break;
case "getmanagers":
if (!empty($VARS['uid'])) {
$emp = new User($VARS['uid']);
} else if (!empty($VARS['username'])) {
$emp = User::byUsername($VARS['username']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
if (!$emp->exists()) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
}
$managers = $database->select('managers', 'managerid', ['employeeid' => $emp->getUID()]);
exit(json_encode(["status" => "OK", "managers" => $managers]));
break;
case "usersearch":
if (is_empty($VARS['search']) || strlen($VARS['search']) < 3) {
exit(json_encode(["status" => "OK", "result" => []]));
}
$data = $database->select('accounts', ['uid', 'username', 'realname (name)'], ["OR" => ['username[~]' => $VARS['search'], 'realname[~]' => $VARS['search']], "LIMIT" => 10]);
exit(json_encode(["status" => "OK", "result" => $data]));
break;
case "permission":
if (empty($VARS['code'])) {
http_response_code(400);
die("\"400 Bad Request\"");
}
$perm = $VARS['code'];
if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
} else if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
if (!$user->exists()) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
}
exit(json_encode(["status" => "OK", "has_permission" => $user->hasPermission($perm)]));
break;
case "mobileenabled":
exit(json_encode(["status" => "OK", "mobile" => MOBILE_ENABLED]));
case "mobilevalid":
if (is_empty($VARS['username']) || is_empty($VARS['code'])) {
http_response_code(400);
die("\"400 Bad Request\"");
}
$code = strtoupper($VARS['code']);
$user_key_valid = $database->has('mobile_codes', ['[>]accounts' => ['uid' => 'uid']], ["AND" => ['mobile_codes.code' => $code, 'accounts.username' => strtolower($VARS['username'])]]);
exit(json_encode(["status" => "OK", "valid" => $user_key_valid]));
case "alertemail":
engageRateLimit();
if (is_empty($VARS['username']) || !User::byUsername($VARS['username'])->exists()) {
http_response_code(400);
die("\"400 Bad Request\"");
}
$appname = "???";
if (!is_empty($VARS['appname'])) {
$appname = $VARS['appname'];
}
$result = User::byUsername($VARS['username'])->sendAlertEmail($appname);
if ($result === TRUE) {
exit(json_encode(["status" => "OK"]));
}
exit(json_encode(["status" => "ERROR", "msg" => $result]));
case "codelogin":
$database->delete("onetimekeys", ["expires[<]" => date("Y-m-d H:i:s")]); // cleanup
if ($database->has("onetimekeys", ["key" => $VARS['code'], "expires[>]" => date("Y-m-d H:i:s")])) {
$user = $database->get("onetimekeys", ["[>]accounts" => ["uid" => "uid"]], ["username", "realname", "accounts.uid"], ["key" => $VARS['code']]);
exit(json_encode(["status" => "OK", "user" => $user]));
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("no such code or code expired", false)]));
}
case "listapps":
$apps = EXTERNAL_APPS;
// Format paths as absolute URLs
foreach ($apps as $k => $v) {
if (strpos($apps[$k]['url'], "http") === FALSE) {
$apps[$k]['url'] = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . ($_SERVER['SERVER_PORT'] != 80 || $_SERVER['SERVER_PORT'] != 443 ? ":" . $_SERVER['SERVER_PORT'] : "") . $apps[$k]['url'];
}
}
exit(json_encode(["status" => "OK", "apps" => $apps]));
case "getusersbygroup":
if ($VARS['gid']) {
if ($database->has("groups", ['groupid' => $VARS['gid']])) {
$groupid = $VARS['gid'];
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("group does not exist", false)]));
}
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
if ($VARS['get'] == "username") {
$users = $database->select('assigned_groups', ['[>]accounts' => ['uid' => 'uid']], 'username', ['groupid' => $groupid, "ORDER" => "username"]);
} else if ($VARS['get'] == "detail") {
$users = $database->select('assigned_groups', ['[>]accounts' => ['uid' => 'uid']], ['username', 'realname (name)', 'accounts.uid', 'pin'], ['groupid' => $groupid, "ORDER" => "realname"]);
for ($i = 0; $i < count($users); $i++) {
if (is_null($users[$i]['pin']) || $users[$i]['pin'] == "") {
$users[$i]['pin'] = false;
} else {
$users[$i]['pin'] = true;
}
}
} else {
$users = $database->select('assigned_groups', 'uid', ['groupid' => $groupid]);
}
exit(json_encode(["status" => "OK", "users" => $users]));
break;
case "getgroupsbyuser":
if ($VARS['uid']) {
if ($database->has("accounts", ['uid' => $VARS['uid']])) {
$empid = $VARS['uid'];
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
}
} else if ($VARS['username']) {
if ($database->has("accounts", ['username' => strtolower($VARS['username'])])) {
$empid = $database->select('accounts', 'uid', ['username' => strtolower($VARS['username'])]);
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
}
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
$groups = $database->select('assigned_groups', ["[>]groups" => ["groupid" => "groupid"]], ['groups.groupid (id)', 'groups.groupname (name)'], ['uid' => $empid]);
exit(json_encode(["status" => "OK", "groups" => $groups]));
break;
case "getgroups":
$groups = $database->select('groups', ['groupid (id)', 'groupname (name)']);
exit(json_encode(["status" => "OK", "groups" => $groups]));
break;
case "groupsearch":
if (is_empty($VARS['search']) || strlen($VARS['search']) < 2) {
exit(json_encode(["status" => "OK", "result" => []]));
}
$data = $database->select('groups', ['groupid (id)', 'groupname (name)'], ['groupname[~]' => $VARS['search'], "LIMIT" => 10]);
exit(json_encode(["status" => "OK", "result" => $data]));
break;
case "checkpin":
$pin = "";
if (is_empty($VARS['pin'])) {
http_response_code(400);
die("\"400 Bad Request\"");
}
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
if ($user->exists()) {
$pin = $database->get("accounts", "pin", ["uid" => $user->getUID()]);
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
}
if (is_null($pin) || $pin == "") {
exit(json_encode(["status" => "ERROR", "pinvalid" => false, "nopinset" => true]));
}
exit(json_encode(["status" => "OK", "pinvalid" => ($pin == $VARS['pin'])]));
break;
case "getnotifications":
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
try {
$notifications = Notifications::get($user);
exit(json_encode(["status" => "OK", "notifications" => $notifications]));
} catch (Exception $ex) {
exit(json_encode(["status" => "ERROR", "msg" => $ex->getMessage()]));
}
break;
case "readnotification":
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
if (empty($VARS['id'])) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("invalid parameters", false)]));
}
try {
Notifications::read($user, $VARS['id']);
exit(json_encode(["status" => "OK"]));
} catch (Exception $ex) {
exit(json_encode(["status" => "ERROR", "msg" => $ex->getMessage()]));
}
break;
case "addnotification":
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
try {
$timestamp = "";
if (!empty($VARS['timestamp'])) {
$timestamp = date("Y-m-d H:i:s", strtotime($VARS['timestamp']));
}
$url = "";
if (!empty($VARS['url'])) {
$url = $VARS['url'];
}
$nid = Notifications::add($user, $VARS['title'], $VARS['content'], $timestamp, $url, isset($VARS['sensitive']));
exit(json_encode(["status" => "OK", "id" => $nid]));
} catch (Exception $ex) {
exit(json_encode(["status" => "ERROR", "msg" => $ex->getMessage()]));
}
break;
case "deletenotification":
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
if (empty($VARS['id'])) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("invalid parameters", false)]));
}
try {
Notifications::delete($user, $VARS['id']);
exit(json_encode(["status" => "OK"]));
} catch (Exception $ex) {
exit(json_encode(["status" => "ERROR", "msg" => $ex->getMessage()]));
}
break;
default:
http_response_code(404);
die(json_encode("404 Not Found: the requested action is not available."));
}
// Load in new API from legacy location (a.k.a. here)
require __DIR__ . "/api/index.php";

@ -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>

@ -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(null, "OK", ["account" => User::byUsername($VARS['username'])->getStatus()->getString()]);

@ -0,0 +1,14 @@
<?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/.
*/
$code = strtoupper(substr(md5(mt_rand() . uniqid("", true)), 0, 20));
$desc = htmlspecialchars($VARS['desc']);
$chunk_code = str_replace(" ", "-", trim(chunk_split($code, 5, ' ')));
$database->insert('apppasswords', ['uid' => User::byUsername($VARS['username'])->getUID(), 'hash' => password_hash($chunk_code, PASSWORD_DEFAULT), 'description' => $desc]);
sendJsonResp("", "OK", ["pass" => $chunk_code]);

@ -0,0 +1,29 @@
<?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/.
*/
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
}
try {
$timestamp = "";
if (!empty($VARS['timestamp'])) {
$timestamp = date("Y-m-d H:i:s", strtotime($VARS['timestamp']));
}
$url = "";
if (!empty($VARS['url'])) {
$url = $VARS['url'];
}
$nid = Notifications::add($user, $VARS['title'], $VARS['content'], $timestamp, $url, isset($VARS['sensitive']));
exitWithJson(["status" => "OK", "id" => $nid]);
} catch (Exception $ex) {
sendJsonResp($ex->getMessage(), "ERROR");
}

@ -0,0 +1,18 @@
<?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/.
*/
engageRateLimit();
$appname = "???";
if (!empty($VARS['appname'])) {
$appname = $VARS['appname'];
}
$result = User::byUsername($VARS['username'])->sendAlertEmail($appname);
if ($result === TRUE) {
sendJsonResp();
}
sendJsonResp($result, "ERROR");

@ -0,0 +1,39 @@
<?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/.
*/
$user = User::byUsername($VARS['username']);
$ok = false;
if (empty($VARS['apppass']) && ($user->checkPassword($VARS['password']) || $user->checkAppPassword($VARS['password']))) {
$ok = true;
} else {
if ((!$user->has2fa() && $user->checkPassword($VARS['password'])) || $user->checkAppPassword($VARS['password'])) {
$ok = true;
}
}
if ($ok) {
Log::insert(LogType::API_AUTH_OK, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
sendJsonResp($Strings->get("login successful", false), "OK");
} else {
Log::insert(LogType::API_AUTH_FAILED, $user->getUID(), "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
if ($user->exists()) {
switch ($user->getStatus()->get()) {
case AccountStatus::LOCKED_OR_DISABLED:
sendJsonResp($Strings->get("account locked", false), "ERROR");
case AccountStatus::TERMINATED:
sendJsonResp($Strings->get("account terminated", false), "ERROR");
case AccountStatus::CHANGE_PASSWORD:
sendJsonResp($Strings->get("password expired", false), "ERROR");
case AccountStatus::NORMAL:
break;
default:
sendJsonResp($Strings->get("account state error", false), "ERROR");
}
}
sendJsonResp($Strings->get("login incorrect", false), "ERROR");
}

@ -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/.
*/
try {
$uid = LoginKey::getuid($VARS['code']);
exitWithJson(["status" => "OK", "uid" => $uid]);
} catch (Exception $ex) {
sendJsonResp("", "ERROR");
}

@ -0,0 +1,24 @@
<?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/.
*/
$pin = "";
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
}
if ($user->exists()) {
$pin = $database->get("accounts", "pin", ["uid" => $user->getUID()]);
} else {
sendJsonResp($Strings->get("login incorrect", false), "ERROR");
}
if (is_null($pin) || $pin == "") {
exitWithJson(["status" => "ERROR", "pinvalid" => false, "nopinset" => true]);
}
exitWithJson(["status" => "OK", "pinvalid" => ($pin == $VARS['pin'])]);

@ -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/.
*/
$database->delete("onetimekeys", ["expires[<]" => date("Y-m-d H:i:s")]); // cleanup
if ($database->has("onetimekeys", ["key" => $VARS['code'], "expires[>]" => date("Y-m-d H:i:s")])) {
$user = $database->get("onetimekeys", ["[>]accounts" => ["uid" => "uid"]], ["username", "realname", "accounts.uid"], ["key" => $VARS['code']]);
exitWithJson(["status" => "OK", "user" => $user]);
} else {
sendJsonResp($Strings->get("no such code or code expired", false), "ERROR");
}

@ -0,0 +1,20 @@
<?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/.
*/
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
}
try {
Notifications::delete($user, $VARS['id']);
sendJsonResp();
} catch (Exception $ex) {
sendJsonResp($ex->getMessage(), "ERROR");
}

@ -0,0 +1,10 @@
<?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/.
*/
$groups = $database->select('groups', ['groupid (id)', 'groupname (name)']);
exitWithJson(["status" => "OK", "groups" => $groups]);

@ -0,0 +1,23 @@
<?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/.
*/
if (!empty($VARS['uid'])) {
if ($database->has("accounts", ['uid' => $VARS['uid']])) {
$empid = $VARS['uid'];
} else {
sendJsonResp($Strings->get("user does not exist", false), "ERROR");
}
} else if (!empty($VARS['username'])) {
if ($database->has("accounts", ['username' => strtolower($VARS['username'])])) {
$empid = $database->select('accounts', 'uid', ['username' => strtolower($VARS['username'])]);
} else {
sendJsonResp($Strings->get("user does not exist", false), "ERROR");
}
}
$groups = $database->select('assigned_groups', ["[>]groups" => ["groupid" => "groupid"]], ['groups.groupid (id)', 'groups.groupname (name)'], ['uid' => $empid]);
exitWithJson(["status" => "OK", "groups" => $groups]);

@ -0,0 +1,22 @@
<?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/.
*/
$appicon = null;
if (!empty($VARS['appicon'])) {
$appicon = $VARS['appicon'];
}
$code = LoginKey::generate($VARS['appname'], $appicon);
if (strpos($SETTINGS['url'], "http") === 0) {
$url = $SETTINGS['url'] . "login/";
} else {
$url = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . (($_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) ? ":" . $_SERVER['SERVER_PORT'] : "") . $SETTINGS['url'] . "login/";
}
exitWithJson(["status" => "OK", "code" => $code, "loginurl" => $url]);

@ -0,0 +1,23 @@
<?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/.
*/
if (!empty($VARS['uid'])) {
$manager = new User($VARS['uid']);
} else if (!empty($VARS['username'])) {
$manager = User::byUsername($VARS['username']);
}
if (!$manager->exists()) {
exitWithJson(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]);
}
if (!empty($VARS['get']) && $VARS['get'] == "username") {
$managed = $database->select('managers', ['[>]accounts' => ['employeeid' => 'uid']], 'username', ['managerid' => $manager->getUID()]);
} else {
$managed = $database->select('managers', 'employeeid', ['managerid' => $manager->getUID()]);
}
exitWithJson(["status" => "OK", "employees" => $managed]);

@ -0,0 +1,19 @@
<?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/.
*/
if (!empty($VARS['uid'])) {
$emp = new User($VARS['uid']);
} else if (!empty($VARS['username'])) {
$emp = User::byUsername($VARS['username']);
}
if (!$emp->exists()) {
exitWithJson(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]);
}
$managers = $database->select('managers', 'managerid', ['employeeid' => $emp->getUID()]);
exitWithJson(["status" => "OK", "managers" => $managers]);

@ -0,0 +1,20 @@
<?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/.
*/
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
}
try {
$notifications = Notifications::get($user);
exitWithJson(["status" => "OK", "notifications" => $notifications]);
} catch (Exception $ex) {
sendJsonResp($ex->getMessage(), "ERROR");
}

@ -0,0 +1,29 @@
<?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/.
*/
if ($database->has("groups", ['groupid' => $VARS['gid']])) {
$groupid = $VARS['gid'];
} else {
sendJsonResp($Strings->get("group does not exist", false), "ERROR");
}
if (!empty($VARS["get"]) && $VARS['get'] == "username") {
$users = $database->select('assigned_groups', ['[>]accounts' => ['uid' => 'uid']], 'username', ['groupid' => $groupid, "ORDER" => "username"]);
} else if (!empty($VARS["get"]) && $VARS['get'] == "detail") {
$users = $database->select('assigned_groups', ['[>]accounts' => ['uid' => 'uid']], ['username', 'realname (name)', 'accounts.uid', 'pin'], ['groupid' => $groupid, "ORDER" => "realname"]);
for ($i = 0; $i < count($users); $i++) {
if (is_null($users[$i]['pin']) || $users[$i]['pin'] == "") {
$users[$i]['pin'] = false;
} else {
$users[$i]['pin'] = true;
}
}
} else {
$users = $database->select('assigned_groups', 'uid', ['groupid' => $groupid]);
}
exitWithJson(["status" => "OK", "users" => $users]);

@ -0,0 +1,13 @@
<?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/.
*/
if (strlen($VARS['search']) < 2) {
exitWithJson(["status" => "OK", "result" => []]);
}
$data = $database->select('groups', ['groupid (id)', 'groupname (name)'], ['groupname[~]' => $VARS['search'], "LIMIT" => 10]);
exitWithJson(["status" => "OK", "result" => $data]);

@ -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(null, "OK", ["otp" => User::byUsername($VARS['username'])->has2fa()]);

@ -0,0 +1,27 @@
<?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/.
*/
if (!empty($VARS['uid']) && $VARS['uid'] == "1") {
$manager = new User($VARS['manager']);
$employee = new User($VARS['employee']);
} else {
$manager = User::byUsername($VARS['manager']);
$employee = User::byUsername($VARS['employee']);
}
if (!$manager->exists()) {
exitWithJson(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false), "user" => $VARS['manager']]);
}
if (!$employee->exists()) {
exitWithJson(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false), "user" => $VARS['employee']]);
}
if ($database->has('managers', ['AND' => ['managerid' => $manager->getUID(), 'employeeid' => $employee->getUID()]])) {
exitWithJson(["status" => "OK", "managerof" => true]);
} else {
exitWithJson(["status" => "OK", "managerof" => false]);
}

@ -0,0 +1,16 @@
<?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/.
*/
$apps = $SETTINGS['apps'];
// Format paths as absolute URLs
foreach ($apps as $k => $v) {
if (strpos($apps[$k]['url'], "http") === FALSE) {
$apps[$k]['url'] = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . ($_SERVER['SERVER_PORT'] != 80 || $_SERVER['SERVER_PORT'] != 443 ? ":" . $_SERVER['SERVER_PORT'] : "") . $apps[$k]['url'];
}
}
exitWithJson(["status" => "OK", "apps" => $apps]);

@ -0,0 +1,46 @@
<?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/.
*/
engageRateLimit();
$user = User::byUsername($VARS['username']);
$ok = false;
if (empty($VARS['apppass']) && ($user->checkPassword($VARS['password']) || $user->checkAppPassword($VARS['password']))) {
$ok = true;
} else {
if ((!$user->has2fa() && $user->checkPassword($VARS['password'])) || $user->checkAppPassword($VARS['password'])) {
$ok = true;
}
}
if ($ok) {
switch ($user->getStatus()->getString()) {
case "LOCKED_OR_DISABLED":
Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exitWithJson(["status" => "ERROR", "msg" => $Strings->get("account locked", false)]);
case "TERMINATED":
Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exitWithJson(["status" => "ERROR", "msg" => $Strings->get("account terminated", false)]);
case "CHANGE_PASSWORD":
Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exitWithJson(["status" => "ERROR", "msg" => $Strings->get("password expired", false)]);
case "NORMAL":
Log::insert(LogType::API_LOGIN_OK, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exitWithJson(["status" => "OK"]);
case "ALERT_ON_ACCESS":
$user->sendAlertEmail();
Log::insert(LogType::API_LOGIN_OK, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exitWithJson(["status" => "OK", "alert" => true]);
default:
Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exitWithJson(["status" => "ERROR", "msg" => $Strings->get("account state error", false)]);
}
} else {
Log::insert(LogType::API_LOGIN_FAILED, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exitWithJson(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]);
}

@ -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/.
*/
exitWithJson(["status" => "OK", "mobile" => $SETTINGS['mobile_enabled']]);

@ -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/.
*/
if (empty($VARS['username']) || empty($VARS['code'])) {
http_response_code(400);
die("\"400 Bad Request\"");
}
$code = strtoupper($VARS['code']);
$user_key_valid = $database->has('mobile_codes', ['[>]accounts' => ['uid' => 'uid']], ["AND" => ['mobile_codes.code' => $code, 'accounts.username' => strtolower($VARS['username'])]]);
exitWithJson(["status" => "OK", "valid" => $user_key_valid]);

@ -0,0 +1,19 @@
<?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/.
*/
$perm = $VARS['code'];
if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
} else if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
}
if (!$user->exists()) {
exitWithJson(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]);
}
exitWithJson(["status" => "OK", "has_permission" => $user->hasPermission($perm)]);

@ -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();

@ -0,0 +1,25 @@
<?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/.
*/
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
if (empty($VARS['id'])) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("invalid parameters", false)]));
}
try {
Notifications::read($user, $VARS['id']);
sendJsonResp();
} catch (Exception $ex) {
sendJsonResp($ex->getMessage(), "ERROR");
}

@ -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/.
*/
if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
} else if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
}
sendJsonResp(null, "OK", ["exists" => $user->exists()]);

@ -0,0 +1,20 @@
<?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/.
*/
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
}
if ($user->exists()) {
$data = $database->get("accounts", ["uid", "username", "realname (name)", "email", "phone" => ["phone1 (1)", "phone2 (2)"], 'pin'], ["uid" => $user->getUID()]);
$data['pin'] = (is_null($data['pin']) || $data['pin'] == "" ? false : true);
sendJsonResp(null, "OK", ["data" => $data]);
} else {
sendJsonResp($Strings->get("login incorrect", false), "ERROR");
}

@ -0,0 +1,13 @@
<?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/.
*/
if (strlen($VARS['search']) < 3) {
exitWithJson(["status" => "OK", "result" => []]);
}
$data = $database->select('accounts', ['uid', 'username', 'realname (name)'], ["OR" => ['username[~]' => $VARS['search'], 'realname[~]' => $VARS['search']], "LIMIT" => 10]);
exitWithJson(["status" => "OK", "result" => $data]);

@ -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/.
*/
$user = User::byUsername($VARS['username']);
if ($user->check2fa($VARS['code'])) {
sendJsonResp(null, "OK", ["valid" => true]);
} else {
Log::insert(LogType::API_BAD_2FA, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
sendJsonResp($Strings->get("2fa incorrect", false), "ERROR", ["valid" => false]);
}

@ -0,0 +1,267 @@
<?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" => [
],
"permission" => [
],
"keytype" => "NONE"
],
"auth" => [
"load" => "auth.php",
"vars" => [
"username" => "string",
"password" => "string",
"apppass (optional)" => "/[0-1]/"
],
"keytype" => "AUTH"
],
"userinfo" => [
"load" => "userinfo.php",
"vars" => [
"OR" => [
"username" => "string",
"uid" => "numeric"
]
],
"keytype" => "READ"
],
"userexists" => [
"load" => "userexists.php",
"vars" => [
"OR" => [
"username" => "string",
"uid" => "numeric"
]
],
"keytype" => "AUTH"
],
"hastotp" => [
"load" => "hastotp.php",
"vars" => [
"username" => "string"
],
"keytype" => "AUTH"
],
"verifytotp" => [
"load" => "verifytotp.php",
"vars" => [
"username" => "string",
"code" => "string"
],
"keytype" => "AUTH"
],
"acctstatus" => [
"load" => "acctstatus.php",
"vars" => [
"username" => "string"
],
"keytype" => "AUTH"
],
"login" => [
"load" => "login.php",
"vars" => [
"username" => "string",
"password" => "string",
"apppass (optional)" => "/[0-1]/"
],
"keytype" => "AUTH"
],
"ismanagerof" => [
"load" => "ismanagerof.php",
"vars" => [
"manager" => "string",
"employee" => "string",
"uid (optional)" => "numeric"
],
"keytype" => "READ"
],
"getmanaged" => [
"load" => "getmanaged.php",
"vars" => [
"OR" => [
"username" => "string",
"uid" => "numeric"
],
"get (optional)" => "string"
],
"keytype" => "READ"
],
"getmanagers" => [
"load" => "getmanagers.php",
"vars" => [
"OR" => [
"username" => "string",
"uid" => "numeric"
]
],
"keytype" => "READ"
],
"usersearch" => [
"load" => "usersearch.php",
"vars" => [
"search" => "string"
],
"keytype" => "READ"
],
"permission" => [
"load" => "permission.php",
"vars" => [
"OR" => [
"username" => "string",
"uid" => "numeric"
],
"code" => "string"
],
"keytype" => "READ"
],
"mobileenabled" => [
"load" => "mobileenabled.php",
"keytype" => "NONE"
],
"mobilevalid" => [
"load" => "mobilevalid.php",
"vars" => [
"username" => "string",
"code" => "string"
],
"keytype" => "AUTH"
],
"alertemail" => [
"load" => "alertemail.php",
"vars" => [
"username" => "string",
"appname (optional)" => "string"
],
"keytype" => "FULL"
],
"codelogin" => [
"load" => "codelogin.php",
"vars" => [
"code" => "string"
],
"keytype" => "AUTH"
],
"listapps" => [
"load" => "listapps.php",
"keytype" => "NONE"
],
"getusersbygroup" => [
"load" => "getusersbygroup.php",
"vars" => [
"gid" => "numeric",
"get (optional)" => "string"
],
"keytype" => "READ"
],
"getgroupsbyuser" => [
"load" => "getgroupsbyuser.php",
"vars" => [
"OR" => [
"uid" => "numeric",
"username" => "string"
]
],
"keytype" => "READ"
],
"getgroups" => [
"load" => "getgroups.php",
"keytype" => "READ"
],
"groupsearch" => [
"load" => "groupsearch.php",
"vars" => [
"search" => "string"
],
"keytype" => "READ"
],
"checkpin" => [
"load" => "checkpin.php",
"vars" => [
"pin" => "string",
"OR" => [
"uid" => "numeric",
"username" => "string"
]
],
"keytype" => "AUTH"
],
"getnotifications" => [
"load" => "getnotifications.php",
"vars" => [
"OR" => [
"uid" => "numeric",
"username" => "string"
]
],
"keytype" => "READ"
],
"readnotification" => [
"load" => "readnotification.php",
"vars" => [
"OR" => [
"uid" => "numeric",
"username" => "string"
],
"id" => "numeric"
],
"keytype" => "FULL"
],
"addnotification" => [
"load" => "addnotification.php",
"vars" => [
"OR" => [
"uid" => "numeric",
"username" => "string"
],
"title" => "string",
"content" => "string",
"timestamp (optional)" => "string",
"url (optional)" => "string",
"sensitive (optional)" => "string"
],
"keytype" => "FULL"
],
"deletenotification" => [
"load" => "deletenotification.php",
"vars" => [
"OR" => [
"uid" => "numeric",
"username" => "string"
],
"id" => "numeric"
],
"keytype" => "FULL"
],
"getloginkey" => [
"load" => "getloginkey.php",
"vars" => [
"appname" => "string",
"appicon (optional)" => "string"
],
"keytype" => "AUTH"
],
"checkloginkey" => [
"load" => "checkloginkey.php",
"vars" => [
"code" => "string"
],
"keytype" => "AUTH"
],
"addapppassword" => [
"load" => "addapppassword.php",
"vars" => [
"desc" => "string",
"username" => "string"
],
"keytype" => "FULL"
],
];

@ -0,0 +1,164 @@
<?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 type $VARS
* @global type $database
* @return bool true if the request should continue, false if the request is bad
*/
function authenticate(): bool {
global $VARS, $database;
if (empty($VARS['key'])) {
return false;
} else {
$key = $VARS['key'];
if ($database->has('apikeys', ['key' => $key]) !== TRUE) {
engageRateLimit();
http_response_code(403);
Log::insert(LogType::API_BAD_KEY, null, "Key: " . $key);
return false;
}
return true;
}
return false;
}
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");
}
}
}
}
/**
* Check if the client API key is allowed to access API functions that require the
* specified API key type.
* @global type $VARS
* @global type $database
* @param string $type The required key type: "NONE", "AUTH", "READ", or "FULL"
* @return bool
*/
function checkkeytype(string $type): bool {
global $VARS, $database;
if (empty($VARS['key'])) {
return false;
} else {
$key = $VARS['key'];
$keytype = $database->get('apikeys', 'type', ['key' => $key]);
$allowedtypes = [];
switch ($type) {
case "NONE":
$allowedtypes = ["NONE", "AUTH", "READ", "FULL"];
break;
case "AUTH":
$allowedtypes = ["AUTH", "READ", "FULL"];
break;
case "READ":
$allowedtypes = ["READ", "FULL"];
break;
case "FULL":
$allowedtypes = ["FULL"];
}
if (!in_array($type, $allowedtypes)) {
http_response_code(403);
Log::insert(LogType::API_BAD_KEY, null, "Key: " . $key);
return false;
}
}
return true;
}

@ -0,0 +1,88 @@
<?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) {
$VARS["key"] = $route[1];
for ($i = 2; $i < count($route); $i++) {
$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()) {
http_response_code(403);
die("403 Unauthorized");
}
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"]);
}
// Assume we need full API access
if (empty($APIACTION["keytype"])) {
$APIACTION["keytype"] = "FULL";
}
if (!checkkeytype($APIACTION["keytype"])) {
die("403 Unauthorized");
}
require_once __DIR__ . "/actions/" . $APIACTION["load"];

@ -1,11 +1,14 @@
<?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";
// If the SVG/JavaScript version of FontAwesome is needed
// Increases overhead by a notable amount
define("FONTAWESOME_USEJS", true);
if ($_SESSION['loggedin'] != true) {
header('Location: index.php');
die("Session expired. Log in again to continue.");
@ -14,7 +17,7 @@ if ($_SESSION['loggedin'] != true) {
require_once __DIR__ . "/pages.php";
$pageid = "home";
if (isset($_GET['page']) && !is_empty($_GET['page'])) {
if (!empty($_GET['page'])) {
$pg = strtolower($_GET['page']);
$pg = preg_replace('/[^0-9a-z_]/', "", $pg);
if (array_key_exists($pg, PAGES) && file_exists(__DIR__ . "/pages/" . $pg . ".php")) {
@ -24,14 +27,19 @@ if (isset($_GET['page']) && !is_empty($_GET['page'])) {
}
}
header("Link: <static/img/logo.svg>; rel=preload; as=image", 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/app.css>; rel=preload; as=style", false);
header("Link: <static/css/fa-svg-with-js.css>; rel=preload; as=style", false);
header("Link: <static/js/fontawesome-all.min.js>; rel=preload; as=script", false);
if (FONTAWESOME_USEJS) {
header("Link: <static/css/svg-with-js.min.css>; rel=preload; as=style", false);
header("Link: <static/js/fontawesome-all.min.js>; rel=preload; as=script", false);
} else {
header("Link: <static/css/fontawesome-all.min.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.min.js>; rel=preload; as=script", false);
header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", false);
?>
<!DOCTYPE html>
<html>
@ -40,18 +48,28 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo SITE_TITLE; ?></title>
<title><?php echo $SETTINGS['site_title']; ?></title>
<link rel="icon" href="static/img/logo.svg">
<link rel="icon" href="static/img/logo.svg" type="image/svg+xml">
<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/app.css" rel="stylesheet">
<link href="static/css/fa-svg-with-js.css" rel="stylesheet">
<script nonce="<?php echo $SECURE_NONCE; ?>">
FontAwesomeConfig = {autoAddCss: false}
</script>
<script src="static/js/fontawesome-all.min.js"></script>
<?php
if (FONTAWESOME_USEJS) {
?>
<link href="static/css/svg-with-js.min.css" rel="stylesheet">
<script nonce="<?php echo $SECURE_NONCE; ?>">
FontAwesomeConfig = {autoAddCss: false}
</script>
<script src="static/js/fontawesome-all.min.js"></script>
<?php
} else {
?>
<link href="static/css/fontawesome-all.min.css" rel="stylesheet">
<?php
}
?>
<?php
// custom page styles
if (isset(PAGES[$pageid]['styles'])) {
@ -66,28 +84,35 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
<?php
// Alert messages
if (isset($_GET['msg']) && !is_empty($_GET['msg']) && array_key_exists($_GET['msg'], MESSAGES)) {
// optional string generation argument
if (!isset($_GET['arg']) || is_empty($_GET['arg'])) {
$alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false);
if (!empty($_GET['msg'])) {
if (array_key_exists($_GET['msg'], MESSAGES)) {
// optional string generation argument
if (empty($_GET['arg'])) {
$alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false);
} else {
$alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
}
$alerttype = MESSAGES[$_GET['msg']]['type'];
$alerticon = "square-o";
switch (MESSAGES[$_GET['msg']]['type']) {
case "danger":
$alerticon = "times";
break;
case "warning":
$alerticon = "exclamation-triangle";
break;
case "info":
$alerticon = "info-circle";
break;
case "success":
$alerticon = "check";
break;
}
} else {
$alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
}
$alerttype = MESSAGES[$_GET['msg']]['type'];
$alerticon = "square-o";
switch (MESSAGES[$_GET['msg']]['type']) {
case "danger":
$alerticon = "times";
break;
case "warning":
$alerticon = "exclamation-triangle";
break;
case "info":
$alerticon = "info-circle";
break;
case "success":
$alerticon = "check";
break;
// We don't have a message for this, so just assume an error and escape stuff.
$alertmsg = htmlentities($Strings->get($_GET['msg'], false));
$alerticon = "times";
$alerttype = "danger";
}
echo <<<END
<div class="row justify-content-center" id="msg-alert-box">
@ -121,7 +146,7 @@ END;
</button>
<a class="navbar-brand py-0 mr-auto" href="app.php">
<img src="static/img/logo.svg" alt="" class="d-none d-<?php echo $navbar_breakpoint; ?>-inline brand-img py-0" />
<?php echo SITE_TITLE; ?>
<?php echo $SETTINGS['site_title']; ?>
</a>
<div class="collapse navbar-collapse py-0" id="navbar-collapse">
@ -177,12 +202,12 @@ END;
?>
</div>
<div class="footer">
<?php echo FOOTER_TEXT; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
<?php echo $SETTINGS['footer_text']; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo $SETTINGS['copyright']; ?>
</div>
</div>
<script src="static/js/jquery-3.3.1.min.js"></script>
<script src="static/js/bootstrap.min.js"></script>
<script src="static/js/bootstrap.bundle.min.js"></script>
<script src="static/js/app.js"></script>
<?php
// custom page scripts

@ -3,13 +3,13 @@
"description": "Single-sign-on system and dashboard for Netsyms Business Apps",
"type": "project",
"require": {
"catfan/medoo": "^1.5",
"catfan/medoo": "^1.7",
"guzzlehttp/guzzle": "^6.5",
"spomky-labs/otphp": "^8.3",
"endroid/qr-code": "^3.2",
"ldaptools/ldaptools": "^0.24.0",
"guzzlehttp/guzzle": "^6.2",
"phpmailer/phpmailer": "^5.2",
"christian-riesen/base32": "^1.3"
"christian-riesen/base32": "^1.3",
"mibe/feedwriter": "^1.1"
},
"license": "MPL-2.0",
"authors": [

792
composer.lock generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

@ -1,5 +1,5 @@
-- MySQL Script generated by MySQL Workbench
-- Sat 28 Jul 2018 03:55:27 PM MDT
-- Mon 11 Feb 2019 04:07:57 PM MST
-- Model: New Model Version: 1.0
-- MySQL Workbench Forward Engineering
@ -70,46 +70,13 @@ CREATE TABLE IF NOT EXISTS `accounts` (
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `apps`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `apps` (
`appid` INT NOT NULL AUTO_INCREMENT,
`appname` VARCHAR(45) NULL,
`appcode` VARCHAR(45) NULL,
PRIMARY KEY (`appid`),
UNIQUE INDEX `appid_UNIQUE` (`appid` ASC))
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `available_apps`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `available_apps` (
`appid` INT NOT NULL,
`uid` INT NOT NULL,
PRIMARY KEY (`appid`, `uid`),
INDEX `fk_apps_has_accounts_accounts1_idx` (`uid` ASC),
INDEX `fk_apps_has_accounts_apps1_idx` (`appid` ASC),
CONSTRAINT `fk_apps_has_accounts_apps1`
FOREIGN KEY (`appid`)
REFERENCES `apps` (`appid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_apps_has_accounts_accounts1`
FOREIGN KEY (`uid`)
REFERENCES `accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `apikeys`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `apikeys` (
`key` VARCHAR(60) NOT NULL,
`notes` TEXT NULL,
`type` VARCHAR(45) NOT NULL DEFAULT 'FULL',
PRIMARY KEY (`key`))
ENGINE = InnoDB;
@ -313,6 +280,80 @@ CREATE TABLE IF NOT EXISTS `notifications` (
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `userkeytypes`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `userkeytypes` (
`typeid` INT NOT NULL,
`typename` VARCHAR(45) NOT NULL,
PRIMARY KEY (`typeid`, `typename`))
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `userkeys`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `userkeys` (
`uid` INT NOT NULL,
`key` VARCHAR(100) NOT NULL,
`created` DATETIME NULL,
`typeid` INT NOT NULL,
PRIMARY KEY (`uid`),
INDEX `fk_userkeys_userkeytypes1_idx` (`typeid` ASC),
CONSTRAINT `fk_userkeys_accounts1`
FOREIGN KEY (`uid`)
REFERENCES `accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_userkeys_userkeytypes1`
FOREIGN KEY (`typeid`)
REFERENCES `userkeytypes` (`typeid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `userloginkeys`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `userloginkeys` (
`id` INT NOT NULL AUTO_INCREMENT,
`key` VARCHAR(255) NOT NULL,
`expires` DATETIME NULL,
`uid` INT NULL,
`appname` VARCHAR(255) NOT NULL,
`appicon` TINYTEXT NULL,
PRIMARY KEY (`id`, `key`),
UNIQUE INDEX `id_UNIQUE` (`id` ASC),
UNIQUE INDEX `key_UNIQUE` (`key` ASC),
INDEX `fk_userloginkeys_accounts1_idx` (`uid` ASC),
CONSTRAINT `fk_userloginkeys_accounts1`
FOREIGN KEY (`uid`)
REFERENCES `accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `apppasswords`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `apppasswords` (
`passid` INT NOT NULL AUTO_INCREMENT,
`hash` VARCHAR(255) NOT NULL,
`uid` INT NOT NULL,
`description` VARCHAR(255) NOT NULL,
PRIMARY KEY (`passid`, `uid`),
UNIQUE INDEX `passid_UNIQUE` (`passid` ASC),
INDEX `fk_apppasswords_accounts1_idx` (`uid` ASC),
CONSTRAINT `fk_apppasswords_accounts1`
FOREIGN KEY (`uid`)
REFERENCES `accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
SET SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;
@ -335,8 +376,7 @@ COMMIT;
-- -----------------------------------------------------
START TRANSACTION;
INSERT INTO `accttypes` (`typeid`, `typecode`) VALUES (1, 'LOCAL');
INSERT INTO `accttypes` (`typeid`, `typecode`) VALUES (2, 'LDAP');
INSERT INTO `accttypes` (`typeid`, `typecode`) VALUES (3, 'LIGHT');
INSERT INTO `accttypes` (`typeid`, `typecode`) VALUES (2, 'EXTERNAL');
COMMIT;
@ -390,3 +430,13 @@ INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (404, 'SITEW
COMMIT;
-- -----------------------------------------------------
-- Data for table `userkeytypes`
-- -----------------------------------------------------
START TRANSACTION;
INSERT INTO `userkeytypes` (`typeid`, `typename`) VALUES (1, 'RSSAtomFeed');
INSERT INTO `userkeytypes` (`typeid`, `typename`) VALUES (2, 'Other');
COMMIT;

@ -0,0 +1,35 @@
/*
* 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/.
*/
CREATE TABLE IF NOT EXISTS `userkeys` (
`uid` INT(11) NOT NULL,
`key` VARCHAR(100) NOT NULL,
`created` DATETIME NULL DEFAULT NULL,
`typeid` INT(11) NOT NULL,
PRIMARY KEY (`uid`),
INDEX `fk_userkeys_userkeytypes1_idx` (`typeid` ASC),
CONSTRAINT `fk_userkeys_accounts1`
FOREIGN KEY (`uid`)
REFERENCES `accounthub`.`accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_userkeys_userkeytypes1`
FOREIGN KEY (`typeid`)
REFERENCES `accounthub`.`userkeytypes` (`typeid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
CREATE TABLE IF NOT EXISTS `userkeytypes` (
`typeid` INT(11) NOT NULL,
`typename` VARCHAR(45) NOT NULL,
PRIMARY KEY (`typeid`, `typename`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
INSERT INTO `userkeytypes` (`typeid`, `typename`) VALUES (1, 'RSSAtomFeed');
INSERT INTO `userkeytypes` (`typeid`, `typename`) VALUES (2, 'Other');

@ -0,0 +1,48 @@
/*
* 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/.
*/
DROP TABLE IF EXISTS `available_apps`;
DROP TABLE IF EXISTS `apps`;
CREATE TABLE IF NOT EXISTS `userloginkeys` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`key` VARCHAR(255) NOT NULL,
`expires` DATETIME NULL DEFAULT NULL,
`uid` INT(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`, `key`),
UNIQUE INDEX `id_UNIQUE` (`id` ASC),
UNIQUE INDEX `key_UNIQUE` (`key` ASC),
INDEX `fk_userloginkeys_accounts1_idx` (`uid` ASC),
CONSTRAINT `fk_userloginkeys_accounts1`
FOREIGN KEY (`uid`)
REFERENCES `accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
ALTER TABLE `userloginkeys`
ADD COLUMN `appname` VARCHAR(255) NOT NULL AFTER `uid`;
ALTER TABLE `userloginkeys`
ADD COLUMN `appicon` TINYTEXT NULL DEFAULT NULL AFTER `appname`;
ALTER TABLE `apikeys`
ADD COLUMN `type` VARCHAR(45) NOT NULL DEFAULT 'FULL' AFTER `notes`;
CREATE TABLE IF NOT EXISTS `apppasswords` (
`passid` INT(11) NOT NULL AUTO_INCREMENT,
`hash` VARCHAR(255) NOT NULL,
`uid` INT(11) NOT NULL,
`description` VARCHAR(255) NOT NULL,
PRIMARY KEY (`passid`, `uid`),
UNIQUE INDEX `passid_UNIQUE` (`passid` ASC),
INDEX `fk_apppasswords_accounts1_idx` (`uid` ASC),
CONSTRAINT `fk_apppasswords_accounts1`
FOREIGN KEY (`uid`)
REFERENCES `accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;

@ -0,0 +1,89 @@
<?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";
date_default_timezone_set('UTC');
use \FeedWriter\RSS1;
use \FeedWriter\RSS2;
use \FeedWriter\ATOM;
if (empty($_GET['key']) || empty($_GET['type'])) {
http_response_code(400);
die("400 Bad Request: please send a user key and a feed type");
}
if (!$database->has('userkeys', ['key' => $_GET['key']])) {
http_response_code(403);
die("403 Forbidden: provide valid key");
}
$uid = $database->get('userkeys', 'uid', ['key' => $_GET['key']]);
$user = new User($uid);
switch ($user->getStatus()->get()) {
case AccountStatus::NORMAL:
case AccountStatus::CHANGE_PASSWORD:
case AccountStatus::ALERT_ON_ACCESS:
break;
default:
http_response_code(403);
die("403 Forbidden: user account not active");
}
$notifications = Notifications::get($user);
switch ($_GET['type']) {
case "rss1":
$feed = new RSS1();
break;
case "rss":
case "rss2":
$feed = new RSS2();
break;
case "atom":
$feed = new ATOM();
break;
default:
http_response_code(400);
die("400 Bad Request: feed parameter must have a value of \"rss\", \"rss1\", \"rss2\" or \"atom\".");
}
$feed->setTitle($Strings->build("Notifications from server for user", ['server' => $SETTINGS['site_title'], 'user' => $user->getName()], false));
if (strpos($SETTINGS['url'], "http") === 0) {
$url = $SETTINGS['url'];
} else {
$url = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . (($_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) ? ":" . $_SERVER['SERVER_PORT'] : "") . $SETTINGS['url'];
}
$feed->setLink($url);
foreach ($notifications as $n) {
$item = $feed->createNewItem();
$item->setTitle($n['title']);
if (empty($n['url'])) {
$item->setLink($url);
} else {
$item->setLink($n['url']);
}
$item->setDate(strtotime($n['timestamp']));
if ($n['sensitive']) {
$content = $Strings->get("Sensitive content hidden", false);
} else {
$content = $n['content'];
}
if ($_GET['type'] == "atom") {
$item->setContent($content);
} else {
$item->setDescription($content);
}
$feed->addItem($item);
}
$feed->printFeed();

@ -1,239 +1,113 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
require_once __DIR__ . "/required.php";
// If we're logged in, we don't need to be here.
if (!empty($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
header('Location: home.php');
// if we're logged in, we don't need to be here.
if (!empty($_SESSION['loggedin']) && $_SESSION['loggedin'] === true && !isset($_GET['permissionerror'])) {
header('Location: app.php');
die();
// This branch will likely run if the user signed in from a different app.
}
/* Authenticate user */
$username_ok = false;
$multiauth = false;
$change_password = false;
if (empty($VARS['progress'])) {
// Easy way to remove "undefined" warnings.
} else if ($VARS['progress'] == "1") {
engageRateLimit();
if (!CAPTCHA_ENABLED || (CAPTCHA_ENABLED && Login::verifyCaptcha($VARS['captcheck_session_code'], $VARS['captcheck_selected_answer'], CAPTCHA_SERVER . "/api.php"))) {
$autherror = "";
$user = User::byUsername($VARS['username']);
if ($user->exists()) {
$status = $user->getStatus()->getString();
switch ($status) {
case "LOCKED_OR_DISABLED":
$alert = $Strings->get("account locked", false);
break;
case "TERMINATED":
$alert = $Strings->get("account terminated", false);
break;
case "CHANGE_PASSWORD":
$alert = $Strings->get("password expired", false);
$alerttype = "info";
$_SESSION['username'] = $user->getUsername();
$_SESSION['uid'] = $user->getUID();
$change_password = true;
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);
Log::insert(LogType::LOGIN_OK, $user->getUID());
header('Location: app.php');
die("Logged in, go to app.php");
}
} else {
$alert = $Strings->get("login incorrect", false);
Log::insert(LogType::LOGIN_FAILED, null, "Username: " . $VARS['username']);
}
}
} else { // User does not exist anywhere
$alert = $Strings->get("login incorrect", false);
Log::insert(LogType::LOGIN_FAILED, null, "Username: " . $VARS['username']);
/**
* 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">
<title><?php echo $SETTINGS['site_title']; ?></title>
<link rel="icon" href="static/img/logo.svg">
<link href="static/css/bootstrap.min.css" rel="stylesheet">
<style nonce="<?php echo $SECURE_NONCE; ?>">
.display-5 {
font-size: 2.5rem;
font-weight: 300;
line-height: 1.2;
}
} else {
$alert = $Strings->get("captcha error", false);
Log::insert(LogType::BAD_CAPTCHA, null, "Username: " . $VARS['username']);
}
} else if ($VARS['progress'] == "2") {
engageRateLimit();
$user = User::byUsername($VARS['username']);
if ($_SESSION['passok'] !== true) {
// stop logins using only username and authcode
sendError("Password integrity check failed!");
}
if ($user->check2fa($VARS['authcode'])) {
Session::start($user);
Log::insert(LogType::LOGIN_OK, $user->getUID());
header('Location: app.php');
die("Logged in, go to app.php");
} else {
$alert = $Strings->get("2fa incorrect", false);
Log::insert(LogType::BAD_2FA, null, "Username: " . $VARS['username']);
}
} else if ($VARS['progress'] == "chpasswd") {
engageRateLimit();
if (!is_empty($_SESSION['username'])) {
$user = User::byUsername($_SESSION['username']);
try {
$result = $user->changePassword($VARS['oldpass'], $VARS['newpass'], $VARS['conpass']);
if ($result === TRUE) {
$alert = $Strings->get(MESSAGES["password_updated"]["string"], false);
$alerttype = MESSAGES["password_updated"]["type"];
}
} catch (PasswordMatchException $e) {
$alert = $Strings->get(MESSAGES["passwords_same"]["string"], false);
$alerttype = "danger";
} catch (PasswordMismatchException $e) {
$alert = $Strings->get(MESSAGES["new_password_mismatch"]["string"], false);
$alerttype = "danger";
} catch (IncorrectPasswordException $e) {
$alert = $Strings->get(MESSAGES["old_password_mismatch"]["string"], false);
$alerttype = "danger";
} catch (WeakPasswordException $e) {
$alert = $Strings->get(MESSAGES["weak_password"]["string"], false);
$alerttype = "danger";
.banner-image {
max-height: 100px;
margin: 2em auto;
border: 1px solid grey;
border-radius: 15%;
}
} else {
session_destroy();
header('Location: index.php');
die();
}
}
</style>
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.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>
<link rel="icon" href="static/img/logo.svg">
<link href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/material-color/material-color.min.css" rel="stylesheet">
<link href="static/css/index.css" rel="stylesheet">
<?php if (CAPTCHA_ENABLED) { ?>
<script src="<?php echo CAPTCHA_SERVER ?>/captcheck.dist.js"></script>
<?php } ?>
</head>
<body>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-auto">
<img class="banner-image" src="static/img/logo.svg" />
<div class="col-12 text-center">
<img class="banner-image" src="./static/img/logo.svg" />
</div>
</div>
<div class="row justify-content-center">
<div class="card col-11 col-xs-11 col-sm-8 col-md-6 col-lg-4">
<div class="card-body">
<h5 class="card-title"><?php $Strings->get("sign in"); ?></h5>
<form action="" method="POST">
<?php
if (!empty($alert)) {
$alerttype = isset($alerttype) ? $alerttype : "danger";
?>
<div class="alert alert-<?php echo $alerttype ?>">
<?php
switch ($alerttype) {
case "danger":
$alerticon = "fas fa-times";
break;
case "warning":
$alerticon = "fas fa-exclamation-triangle";
break;
case "info":
$alerticon = "fas fa-info-circle";
break;
case "success":
$alerticon = "fas fa-check";
break;
default:
$alerticon = "far fa-square";
}
?>
<i class="<?php echo $alerticon ?> fa-fw"></i> <?php echo $alert ?>
</div>
<?php
}
if (!$multiauth && !$change_password) {
?>
<input type="text" class="form-control" name="username" placeholder="<?php $Strings->get("username"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="password" class="form-control" name="password" placeholder="<?php $Strings->get("password"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
<?php if (CAPTCHA_ENABLED) { ?>
<div class="captcheck_container" data-stylenonce="<?php echo $SECURE_NONCE; ?>"></div>
<br />
<?php } ?>
<input type="hidden" name="progress" value="1" />
<?php
} else if ($multiauth) {
?>
<div class="alert alert-info">
<?php $Strings->get("2fa prompt"); ?>
</div>
<input type="text" class="form-control" name="authcode" placeholder="<?php $Strings->get("authcode"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="hidden" name="progress" value="2" />
<input type="hidden" name="username" value="<?php echo $VARS['username']; ?>" />
<?php
} else if ($change_password) {
?>
<input type="password" class="form-control" name="oldpass" placeholder="Current password" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="password" class="form-control" name="newpass" placeholder="New password" required="required" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
<input type="password" class="form-control" name="conpass" placeholder="New password (again)" required="required" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
<input type="hidden" name="progress" value="chpasswd" />
<?php
}
?>
<button type="submit" class="btn btn-primary">
<?php $Strings->get("continue"); ?>
</button>
</form>
<div class="col-12 text-center">
<h1 class="display-5 mb-4"><?php $Strings->get($title); ?></h1>
</div>
<div class="col-12 col-sm-8 col-lg-6">
<div class="card mt-4">
<div class="card-body">
<a href="<?php echo $url; ?>" class="btn btn-primary btn-block"><?php $Strings->get($button); ?></a>
</div>
</div>
</div>
</div>
<div class="footer">
<?php echo FOOTER_TEXT; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
</div>
</div>
<script src="static/js/jquery-3.3.1.min.js"></script>
<script src="static/js/bootstrap.min.js"></script>
</body>
</html>
<?php
}
if (!empty($_GET['logout'])) {
showHTML("You have been logged out.", "Log in again", "./index.php");
die();
}
if (empty($_SESSION["login_code"])) {
$redirecttologin = true;
} else {
try {
$uid = LoginKey::getuid($_SESSION["login_code"]);
$user = new User($uid);
Session::start($user);
$_SESSION["login_code"] = null;
header('Location: app.php');
showHTML("Logged in", "Continue", "./app.php");
die();
} catch (Exception $ex) {
$redirecttologin = true;
}
}
if ($redirecttologin) {
try {
$code = LoginKey::generate($SETTINGS["site_title"], "../static/img/logo.svg");
$_SESSION["login_code"] = $code;
$loginurl = "./login/?code=" . htmlentities($code) . "&redirect=" . htmlentities($_SERVER["REQUEST_URI"]);
header("Location: $loginurl");
showHTML("Continue", "Continue", $loginurl);
die();
} catch (Exception $ex) {
sendError($ex->getMessage());
}
}

@ -3,7 +3,7 @@
"2fa removed": "2-factor authentication disabled.",
"2fa enabled": "2-factor authentication activated.",
"remove 2fa": "Disable 2-factor authentication",
"2fa explained": "2-factor authentication adds more security to your account. You can use the Auth Keys (key icon) feature of the Netsyms Business Mobile app, or another TOTP-enabled app (Authy, FreeOTP, etc) on your smartphone. When you have the app installed, you can enable 2-factor authentication by clicking the button below and scanning a QR code with the app. Whenever you sign in in the future, you'll need to input a six-digit code from your phone into the login page when prompted. You can disable 2-factor authentication from this page if you change your mind.",
"2fa explained": "2-factor authentication adds more security to your account. You can use the Auth Keys (key icon) feature of the Netsyms mobile app, or another TOTP-enabled app (Authy, FreeOTP, etc) on your smartphone. When you have the app installed, you can enable 2-factor authentication by clicking the button below and scanning a QR code with the app. Whenever you sign in in the future, you'll need to input a six-digit code from your phone into the login page when prompted. You can disable 2-factor authentication from this page if you change your mind.",
"2fa active": "2-factor authentication is active on your account. To remove 2fa, reset your authentication secret, or change to a new security device, click the button below.",
"enable 2fa": "Enable 2-factor authentication",
"scan 2fa qrcode": "Scan the QR Code with the authenticator app, or enter the information manually. Then type in the six-digit code the app gives you and press Finish Setup.",
@ -12,5 +12,6 @@
"secret key": "Secret key",
"label": "Label",
"issuer": "Issuer",
"no such code or code expired": "That code is incorrect or expired."
"no such code or code expired": "That code is incorrect or expired.",
"2-factor is enabled, you need to use the QR code or manual setup for security reasons": "2-factor is enabled, you need to use the QR code or manual setup for security reasons."
}

@ -0,0 +1,11 @@
{
"App Passwords": "App Passwords",
"app passwords explained": "Use app passwords instead of your actual password when logging into apps with your {site_name} login. App passwords are required in some places when you have 2-factor authentication enabled.",
"app password setup instructions": "Use the username and password below to log in to {app_name}. You'll only be shown this password one time.",
"App name": "App name",
"Generate password": "Generate password",
"Revoke password": "Revoke password",
"You don't have any app passwords.": "You don't have any app passwords.",
"Done": "Done",
"App passwords are not allowed here.": "App passwords are not allowed here."
}

@ -1,4 +1,5 @@
{
"Create virtual notes and lists": "Create virtual notes and lists",
"Punch in and check work schedule": "Punch in and check work schedule",
"Manage physical items": "Manage physical items",
"Create and publish e-newsletters": "Create and publish e-newsletters",
@ -6,6 +7,7 @@
"Checkout customers and manage online orders": "Checkout customers and manage online orders",
"Build websites and manage contact form messages": "Build websites and manage contact form messages",
"Track jobs and assigned tasks": "Track jobs and assigned tasks",
"Change password, setup 2-factor, and change Station PIN": "Change password, setup 2-factor, and change Station PIN",
"Connect mobile devices to AccountHub": "Connect mobile devices to AccountHub"
"Change password, setup 2-factor, and add app passwords": "Change password, setup 2-factor, and add app passwords",
"Change password, setup 2-factor, add app passwords, and change PIN": "Change password, setup 2-factor, add app passwords, and change PIN",
"Connect mobile devices to {name} and get notifications": "Connect mobile devices to {name} and get notifications"
}

@ -1,26 +1,7 @@
{
"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",
"404 error": "404 Error",
"page not found": "Page not found.",
"invalid parameters": "Invalid request parameters.",
"login server error": "The login server returned an error: {arg}",
"login server user data error": "The login server refused to provide account information. Try again or contact technical support.",
"captcha error": "There was a problem with the CAPTCHA (robot test). Try again.",
"no access permission": "You do not have permission to access this system.",
"generic op error": "An unknown error occurred. Try again later.",
"home": "Home"
"login server error": "The login server returned an error: {arg}"
}

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

@ -0,0 +1,25 @@
{
"Login to {app}": "Login to {app}",
"Username not found.": "Username not found.",
"Password for {user}": "Password for {user}",
"Password incorrect.": "Password incorrect.",
"Two-factor code": "Two-factor code",
"Code incorrect.": "Code incorrect.",
"Current password for {user}": "Current password for {user}",
"New password": "New password",
"New password (again)": "New password (again)",
"Fill in all three boxes.": "Fill in all three boxes.",
"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.",
"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.",
"Back": "Back"
}

@ -3,5 +3,7 @@
"Notification deleted.": "Notification deleted.",
"Mark as read": "Mark as read",
"Delete": "Delete",
"All caught up!": "All caught up!"
"All caught up!": "All caught up!",
"Notifications from server for user": "Notifications from {server} for {user}",
"Sensitive content hidden": "Sensitive content hidden"
}

@ -0,0 +1,15 @@
{
"Create Account": "Create Account",
"Account Created": "Account Created",
"Choose a username.": "Choose a username.",
"Choose a password.": "Choose a password.",
"Enter your name.": "Enter your name.",
"Username already taken, pick another.": "Username already taken, pick another.",
"Your password must be at least {n} characters long.": "Your password must be at least {n} characters long.",
"That email address doesn't look right.": "That email address doesn't look right.",
"Please enter your username (4-100 characters, alphanumeric).": "Please enter your username (4-100 characters, alphanumeric).",
"That password is one of the most popular and insecure ever, make a better one.": "That password is one of the most popular and insecure ever, make a better one.",
"Account creation not allowed. Contact the site administrator for an account.": "Account creation not allowed. Contact the site administrator for an account.",
"CAPTCHA answer incorrect.": "CAPTCHA answer incorrect.",
"That email address is already in use.": "That email address is already in use."
}

@ -1,7 +1,7 @@
{
"sync mobile": "Sync Mobile App",
"scan sync qrcode": "Scan this code with the mobile app or enter the code manually.",
"sync explained": "Access your account and apps on the go. Use a sync code to securely connect your phone or tablet to AccountHub with the Netsyms Business mobile app.",
"sync explained": "Access your account and apps on the go. Use a sync code to securely connect your phone or tablet to {site_name} with the Netsyms mobile app.",
"generate sync": "Create new sync code",
"active sync codes": "Active codes",
"no active codes": "No active codes.",
@ -9,5 +9,9 @@
"manual setup": "Manual Setup:",
"sync key": "Sync key:",
"url": "URL:",
"sync code name": "Device nickname"
"sync code name": "Device nickname",
"notification feed explained": "You can receive notifications via a RSS or ATOM news reader by clicking one of the buttons or manually adding a URL. Click the Reset button if you think someone else might know your feed URL (you'll need to delete and re-add the feed on all your devices).",
"Reset": "Reset",
"Feed key reset.": "Feed key reset.",
"Revoke key": "Revoke key"
}

@ -4,5 +4,6 @@
"account options": "Account options",
"sync": "Sync settings",
"settings": "Settings",
"account": "Account"
"account": "Account",
"Home": "Home"
}

@ -72,5 +72,9 @@ define("MESSAGES", [
"notification_deleted" => [
"string" => "Notification deleted.",
"type" => "success"
],
"feed_key_reset" => [
"string" => "Feed key reset.",
"type" => "success"
]
]);

@ -32,4 +32,4 @@ class PasswordMismatchException extends Exception {
parent::__construct($message, $code, $previous);
}
}
}

@ -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;
}
}

@ -0,0 +1,135 @@
<?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 IPUtils {
/**
* Check if a given ipv4 address is in a given cidr
* @param string $ip IP to check in IPV4 format eg. 127.0.0.1
* @param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed
* @return boolean true if the ip is in this range / false if not.
* @author Thorsten Ott <https://gist.github.com/tott/7684443>
*/
public static function ip4_in_cidr($ip, $cidr) {
if (strpos($cidr, '/') == false) {
$cidr .= '/32';
}
// $range is in IP/CIDR format eg 127.0.0.1/24
list( $cidr, $netmask ) = explode('/', $cidr, 2);
$range_decimal = ip2long($cidr);
$ip_decimal = ip2long($ip);
$wildcard_decimal = pow(2, ( 32 - $netmask)) - 1;
$netmask_decimal = ~ $wildcard_decimal;
return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
}
/**
* Check if a given ipv6 address is in a given cidr
* @param string $ip IP to check in IPV6 format
* @param string $cidr CIDR netmask
* @return boolean true if the IP is in this range, false otherwise.
* @author MW. <https://stackoverflow.com/a/7952169>
*/
public static function ip6_in_cidr($ip, $cidr) {
$address = inet_pton($ip);
$subnetAddress = inet_pton(explode("/", $cidr)[0]);
$subnetMask = explode("/", $cidr)[1];
$addr = str_repeat("f", $subnetMask / 4);
switch ($subnetMask % 4) {
case 0:
break;
case 1:
$addr .= "8";
break;
case 2:
$addr .= "c";
break;
case 3:
$addr .= "e";
break;
}
$addr = str_pad($addr, 32, '0');
$addr = pack("H*", $addr);
$binMask = $addr;
return ($address & $binMask) == $subnetAddress;
}
/**
* Check if the REMOTE_ADDR is on Cloudflare's network.
* @return boolean true if it is, otherwise false
*/
public static function validateCloudflare() {
if (filter_var($_SERVER["REMOTE_ADDR"], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
// Using IPv6
$cloudflare_ips_v6 = [
"2400:cb00::/32",
"2405:8100::/32",
"2405:b500::/32",
"2606:4700::/32",
"2803:f800::/32",
"2c0f:f248::/32",
"2a06:98c0::/29"
];
$valid = false;
foreach ($cloudflare_ips_v6 as $cidr) {
if ($this::ip6_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) {
$valid = true;
break;
}
}
} else {
// Using IPv4
$cloudflare_ips_v4 = [
"103.21.244.0/22",
"103.22.200.0/22",
"103.31.4.0/22",
"104.16.0.0/12",
"108.162.192.0/18",
"131.0.72.0/22",
"141.101.64.0/18",
"162.158.0.0/15",
"172.64.0.0/13",
"173.245.48.0/20",
"188.114.96.0/20",
"190.93.240.0/20",
"197.234.240.0/22",
"198.41.128.0/17"
];
$valid = false;
foreach ($cloudflare_ips_v4 as $cidr) {
if ($this::ip4_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) {
$valid = true;
break;
}
}
}
return $valid;
}
/**
* Makes a good guess at the client's real IP address.
*
* @return string Client IP or `0.0.0.0` if we can't find anything
*/
public static function getClientIP() {
// If CloudFlare is in the mix, we should use it.
// Check if the request is actually from CloudFlare before trusting it.
if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
if ($this::validateCloudflare()) {
return $_SERVER["HTTP_CF_CONNECTING_IP"];
}
}
if (isset($_SERVER["REMOTE_ADDR"])) {
return $_SERVER["REMOTE_ADDR"];
}
return "0.0.0.0"; // This will not happen unless we aren't a web server
}
}

@ -18,7 +18,7 @@ class Log {
public static function insert($type, $user, string $data = "") {
global $database;
// find IP address
$ip = getClientIP();
$ip = IPUtils::getClientIP();
if (gettype($type) == "object" && is_a($type, "LogType")) {
$type = $type->getType();
}

@ -0,0 +1,33 @@
<?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 LoginKey {
public static function generate(string $appname, $appicon = null): string {
global $database;
do {
$code = base64_encode(random_bytes(32));
} while ($database->has('userloginkeys', ['key' => $code]));
$database->insert('userloginkeys', ['key' => $code, 'expires' => date("Y-m-d H:i:s", time() + 600), 'appname' => $appname, 'appicon' => $appicon]);
return $code;
}
public static function getuid(string $code): int {
global $database;
if (!$database->has('userloginkeys', ["AND" => ['key' => $code, 'uid[!]' => null]])) {
throw new Exception();
}
$uid = $database->get('userloginkeys', 'uid', ['key' => $code]);
return $uid;
}
}

@ -42,13 +42,18 @@ class Notifications {
* Fetch all notifications for a user.
* @global $database
* @param User $user
* @param bool $all If false, only returns unseen notifications.
* @return array
* @throws Exception
*/
public static function get(User $user) {
public static function get(User $user, bool $all = true) {
global $database, $Strings;
if ($user->exists()) {
$notifications = $database->select('notifications', ['notificationid (id)', 'timestamp', 'title', 'content', 'url', 'seen', 'sensitive'], ['uid' => $user->getUID(), 'ORDER' => ['seen', 'timestamp' => 'DESC']]);
if ($all) {
$notifications = $database->select('notifications', ['notificationid (id)', 'timestamp', 'title', 'content', 'url', 'seen', 'sensitive'], ['uid' => $user->getUID(), 'ORDER' => ['seen', 'timestamp' => 'DESC']]);
} else {
$notifications = $database->select('notifications', ['notificationid (id)', 'timestamp', 'title', 'content', 'url', 'seen', 'sensitive'], ["AND" => ['uid' => $user->getUID(), 'seen' => 0], 'ORDER' => ['timestamp' => 'DESC']]);
}
for ($i = 0; $i < count($notifications); $i++) {
$notifications[$i]['id'] = $notifications[$i]['id'] * 1;
$notifications[$i]['seen'] = ($notifications[$i]['seen'] == "1" ? true : false);
@ -90,4 +95,5 @@ class Notifications {
}
throw new Exception($Strings->get("user does not exist", false));
}
}

@ -0,0 +1,31 @@
<?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 RandomString {
/**
* Generate a random string, using a cryptographically secure
* pseudorandom number generator (random_int)
*
* From https://stackoverflow.com/a/31107425
*
* @param int $length How many characters do we want?
* @param string $keyspace A string of all possible characters
* to select from
* @return string
*/
public static function generate(int $length, string $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'): string {
$pieces = [];
$max = mb_strlen($keyspace, '8bit') - 1;
for ($i = 0; $i < $length; ++$i) {
$pieces [] = $keyspace[random_int(0, $max)];
}
return implode('', $pieces);
}
}

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

@ -19,6 +19,7 @@ class User {
private $authsecret;
private $has2fa = false;
private $exists = false;
private $apppasswords = [];
public function __construct(int $uid, string $username = "") {
global $database;
@ -32,6 +33,7 @@ class User {
$this->authsecret = $user['authsecret'];
$this->has2fa = !empty($user['authsecret']);
$this->exists = true;
$this->apppasswords = $database->select('apppasswords', 'hash', ['uid' => $this->uid]);
} else {
$this->uid = $uid;
$this->username = $username;
@ -63,7 +65,7 @@ class User {
global $database;
$database->insert('accounts', [
'username' => strtolower($username),
'password' => (is_null($password) ? null : encryptPassword($password)),
'password' => (is_null($password) ? null : password_hash($password, PASSWORD_BCRYPT)),
'realname' => $realname,
'email' => $email,
'phone1' => $phone1,
@ -107,6 +109,20 @@ class User {
return password_verify($password, $this->passhash);
}
/**
* Check the given password against the user's app passwords.
* @param string $apppassword
* @return bool
*/
function checkAppPassword(string $apppassword): bool {
foreach ($this->apppasswords as $hash) {
if (password_verify($apppassword, $hash)) {
return true;
}
}
return false;
}
/**
* Change the user's password.
* @global $database $database
@ -119,7 +135,7 @@ class User {
* @throws WeakPasswordException
*/
function changePassword(string $old, string $new, string $new2) {
global $database;
global $database, $SETTINGS;
if ($old == $new) {
throw new PasswordMatchException();
}
@ -137,7 +153,7 @@ class User {
if ($passrank !== FALSE) {
throw new WeakPasswordException();
}
if (strlen($new) < MIN_PASSWORD_LENGTH) {
if (strlen($new) < $SETTINGS['min_password_length']) {
throw new WeakPasswordException();
}
@ -146,6 +162,7 @@ class User {
return true;
}
function check2fa(string $code): bool {
if (!$this->has2fa) {
return true;
@ -171,10 +188,11 @@ class User {
* @return string OTP provisioning URI (for generating a QR code)
*/
function generate2fa(): string {
global $SETTINGS;
$secret = random_bytes(20);
$encoded_secret = Base32::encode($secret);
$totp = new TOTP((empty($this->email) ? $this->realname : $this->email), $encoded_secret);
$totp->setIssuer(SYSTEM_NAME);
$totp->setIssuer($SETTINGS['system_name']);
return $totp->getProvisioningUri();
}
@ -214,29 +232,33 @@ class User {
return new AccountStatus($statuscode);
}
function sendAlertEmail(string $appname = SITE_TITLE) {
if (is_empty(ADMIN_EMAIL) || filter_var(ADMIN_EMAIL, FILTER_VALIDATE_EMAIL) === FALSE) {
function sendAlertEmail(string $appname = null) {
global $SETTINGS, $Strings;
if (is_null($appname)) {
$appname = $SETTINGS['site_title'];
}
if (empty($SETTINGS["email"]["admin_email"]) || filter_var($SETTINGS["email"]["admin_email"], FILTER_VALIDATE_EMAIL) === FALSE) {
return "invalid_to_email";
}
if (is_empty(FROM_EMAIL) || filter_var(FROM_EMAIL, FILTER_VALIDATE_EMAIL) === FALSE) {
if (empty($SETTINGS["email"]["from"]) || filter_var($SETTINGS["email"]["from"], FILTER_VALIDATE_EMAIL) === FALSE) {
return "invalid_from_email";
}
$mail = new PHPMailer;
if (DEBUG) {
if ($SETTINGS['debug']) {
$mail->SMTPDebug = 2;
}
if (USE_SMTP) {
if ($SETTINGS['email']['use_smtp']) {
$mail->isSMTP();
$mail->Host = SMTP_HOST;
$mail->SMTPAuth = SMTP_AUTH;
$mail->Username = SMTP_USER;
$mail->Password = SMTP_PASS;
$mail->SMTPSecure = SMTP_SECURE;
$mail->Port = SMTP_PORT;
if (SMTP_ALLOW_INVALID_CERTIFICATE) {
$mail->Host = $SETTINGS['email']['host'];
$mail->SMTPAuth = $SETTINGS['email']['auth'];
$mail->Username = $SETTINGS['email']['user'];
$mail->Password = $SETTINGS['email']['password'];
$mail->SMTPSecure = $SETTINGS['email']['secure'];
$mail->Port = $SETTINGS['email']['port'];
if ($SETTINGS['email']['allow_invalid_certificate']) {
$mail->SMTPOptions = array(
'ssl' => array(
'verify_peer' => false,
@ -247,11 +269,11 @@ class User {
}
}
$mail->setFrom(FROM_EMAIL, 'Account Alerts');
$mail->addAddress(ADMIN_EMAIL, "System Admin");
$mail->setFrom($SETTINGS["email"]["from"], 'Account Alerts');
$mail->addAddress($SETTINGS["email"]["admin_email"], "System Admin");
$mail->isHTML(false);
$mail->Subject = $Strings->get("admin alert email subject", false);
$mail->Body = $Strings->build("admin alert email message", ["username" => $this->username, "datetime" => date("Y-m-d H:i:s"), "ipaddr" => getClientIP(), "appname" => $appname], false);
$mail->Body = $Strings->build("admin alert email message", ["username" => $this->username, "datetime" => date("Y-m-d H:i:s"), "ipaddr" => IPUtils::getClientIP(), "appname" => $appname], false);
if (!$mail->send()) {
return $mail->ErrorInfo;

@ -0,0 +1,169 @@
<?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";
if (empty($_GET['code']) || empty($_GET['redirect'])) {
die("Bad request.");
}
// Delete old keys to keep the table small and tidy
$database->delete("userloginkeys", ["expires[<]" => date("Y-m-d H:i:s")]);
if (!$database->has("userloginkeys", ["AND" => ["key" => $_GET["code"]], "expires[>]" => date("Y-m-d H:i:s"), "uid" => null])) {
header("Location: $_GET[redirect]");
die("Invalid auth code.");
}
$APPINFO = $database->get("userloginkeys", ["appname", "appicon"], ["key" => $_GET["code"]]);
$APPNAME = $APPINFO["appname"];
$APPICON = $APPINFO["appicon"];
if (empty($_SESSION['thisstep'])) {
$_SESSION['thisstep'] = "username";
}
if (!empty($_GET['reset'])) {
$_SESSION['thisstep'] = "username";
$_SESSION['check'] = "";
header("Location: ./?code=$_GET[code]&redirect=$_GET[redirect]");
}
$error = "";
function sendUserBack($code, $url, $uid) {
global $database;
$_SESSION['check'] = null;
$_SESSION['thisstep'] = null;
$_SESSION['login_uid'] = null;
$_SESSION['login_pwd'] = null;
$database->update("userloginkeys", ["uid" => $uid], ["key" => $code]);
Log::insert(LogType::LOGIN_OK, $uid);
header("Location: $url");
die("<a href=\"" . htmlspecialchars($url) . "\">Click here</a>");
}
if (!empty($_SESSION['check'])) {
switch ($_SESSION['check']) {
case "username":
if (empty($_POST['username'])) {
$_SESSION['thisstep'] = "username";
break;
}
$user = User::byUsername($_POST['username']);
if ($user->exists()) {
$_SESSION['login_uid'] = $user->getUID();
switch ($user->getStatus()->get()) {
case AccountStatus::LOCKED_OR_DISABLED:
$error = $Strings->get("account locked", false);
break;
case AccountStatus::TERMINATED:
$error = $Strings->get("account terminated", false);
break;
case AccountStatus::ALERT_ON_ACCESS:
$mail_resp = $user->sendAlertEmail();
case AccountStatus::NORMAL:
$_SESSION['thisstep'] = "password";
break;
case AccountStatus::CHANGE_PASSWORD:
$_SESSION['thisstep'] = "change_password";
break;
}
} else {
$error = $Strings->get("Username not found.", false);
Log::insert(LogType::LOGIN_FAILED, null, "Username: " . $user->getUsername());
}
break;
case "password":
if (empty($_POST['password'])) {
$_SESSION['thisstep'] = "password";
break;
}
if (empty($_SESSION['login_uid'])) {
$_SESSION['thisstep'] = "username";
break;
}
$user = new User($_SESSION['login_uid']);
if ($user->checkPassword($_POST['password'])) {
$_SESSION['login_pwd'] = true;
if ($user->has2fa()) {
$_SESSION['thisstep'] = "totp";
} else {
sendUserBack($_GET['code'], $_GET['redirect'], $_SESSION['login_uid']);
}
} else {
$error = $Strings->get("Password incorrect.", false);
if ($user->checkAppPassword($_POST['password'])) {
$error = $Strings->get("App passwords are not allowed here.", false);
}
Log::insert(LogType::LOGIN_FAILED, $user);
}
break;
case "change_password":
if (empty($_POST['oldpassword']) || empty($_POST['newpassword']) || empty($_POST['newpassword2'])) {
$_SESSION['thisstep'] = "change_password";
$error = $Strings->get("Fill in all three boxes.", false);
break;
}
$user = new User($_SESSION['login_uid']);
try {
$result = $user->changePassword($_POST['oldpassword'], $_POST['newpassword'], $_POST['newpassword2']);
if ($result === TRUE) {
if ($user->has2fa()) {
$_SESSION['thisstep'] = "totp";
} else {
sendUserBack($_GET['code'], $_GET['redirect'], $_SESSION['login_uid']);
}
}
} catch (PasswordMatchException $e) {
$error = $Strings->get(MESSAGES["passwords_same"]["string"], false);
} catch (PasswordMismatchException $e) {
$error = $Strings->get(MESSAGES["new_password_mismatch"]["string"], false);
} catch (IncorrectPasswordException $e) {
$error = $Strings->get(MESSAGES["old_password_mismatch"]["string"], false);
} catch (WeakPasswordException $e) {
$error = $Strings->get(MESSAGES["weak_password"]["string"], false);
}
break;
case "totp":
if (empty($_POST['totp']) || empty($_SESSION['login_uid'])) {
$_SESSION['thisstep'] = "username";
break;
}
$user = new User($_SESSION['login_uid']);
if ($user->check2fa($_POST['totp'])) {
sendUserBack($_GET['code'], $_GET['redirect'], $_SESSION['login_uid']);
} else {
$error = $Strings->get("Code incorrect.", false);
Log::insert(LogType::BAD_2FA, null, "Username: " . $user->getUsername());
}
break;
}
}
include __DIR__ . "/parts/header.php";
switch ($_SESSION['thisstep']) {
case "username":
require __DIR__ . "/parts/username.php";
break;
case "password":
require __DIR__ . "/parts/password.php";
break;
case "change_password":
require __DIR__ . "/parts/change_password.php";
break;
case "totp":
require __DIR__ . "/parts/totp.php";
break;
}
include __DIR__ . "/parts/footer.php";

@ -0,0 +1,51 @@
<?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/.
*/
$_SESSION['check'] = "change_password";
$username = (new User($_SESSION['login_uid']))->getUsername();
?>
<form action="" method="POST">
<div>
<?php $Strings->get("password expired"); ?>
</div>
<div class="form-group">
<label for="oldpassword"><?php $Strings->build("Current password for {user}", ["user" => htmlentities($username)]); ?></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
</div>
<input type="password" class="form-control" id="oldpassword" name="oldpassword" placeholder="" required autofocus>
</div>
</div>
<div class="form-group">
<label for="newpassword"><?php $Strings->get("New password"); ?></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
</div>
<input type="password" class="form-control" id="newpassword" name="newpassword" placeholder="" required>
</div>
</div>
<div class="form-group">
<label for="newpassword2"><?php $Strings->get("New password (again)"); ?></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
</div>
<input type="password" class="form-control" id="newpassword2" name="newpassword2" placeholder="" required>
</div>
</div>
<div class="d-flex">
<button type="submit" class="btn btn-primary ml-auto">
<i class="fas fa-chevron-right"></i> <?php $Strings->get("continue"); ?>
</button>
</div>
</form>

@ -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/.
*/
?>
</div>
</div>
</div>
</div>
</div>
<script src="../static/js/fontawesome-all.min.js"></script>

@ -0,0 +1,59 @@
<?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/.
*/
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/login.css>; rel=preload; as=style", false);
header("Link: <../static/css/svg-with-js.min.css>; rel=preload; as=style", false);
header("Link: <../static/js/fontawesome-all.min.js>; rel=preload; as=script", false);
?>
<!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">
<title><?php echo $SETTINGS['site_title']; ?></title>
<link rel="icon" href="../static/img/logo.svg">
<link href="../static/css/bootstrap.min.css" rel="stylesheet">
<link href="../static/css/login.css" rel="stylesheet">
<link href="../static/css/svg-with-js.min.css" rel="stylesheet">
<div class="container mt-4">
<div class="row justify-content-center">
<?php
if (!empty($APPICON)) {
?>
<div class="col-12 text-center">
<img class="banner-image" src="<?php echo $APPICON; ?>" />
</div>
<?php
} else {
?>
<div class="col-12">
<div class="blank-image"></div>
</div>
<?php
}
?>
<div class="col-12 text-center">
<h1 class="display-5 mb-4"><?php $Strings->build("Login to {app}", ["app" => htmlentities($APPNAME)]); ?></h1>
</div>
<div class="col-12 col-sm-8 col-lg-6">
<div class="card mt-4">
<div class="card-body">
<?php
if (!empty($error)) {
?>
<div class="text-danger">
<?php echo $error; ?>
</div>
<?php
}
?>

@ -0,0 +1,32 @@
<?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/.
*/
$_SESSION['check'] = "password";
$username = (new User($_SESSION['login_uid']))->getUsername();
?>
<form action="" method="POST">
<div class="form-group">
<label for="password"><?php $Strings->build("Password for {user}", ["user" => htmlentities($username)]); ?></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
</div>
<input type="password" class="form-control" id="password" name="password" aria-describedby="passwordHelp" placeholder="" required autofocus>
</div>
<small id="passwordHelp" class="form-text text-muted">Enter your password.</small>
</div>
<div class="d-flex">
<a href="./?code=<?php echo htmlentities($_GET['code']); ?>&amp;redirect=<?php echo htmlentities($_GET['redirect']); ?>&amp;reset=1" class="btn btn-link mr-2">
<i class="fas fa-chevron-left"></i> <?php $Strings->get("Back"); ?>
</a>
<button type="submit" class="btn btn-primary ml-auto">
<i class="fas fa-chevron-right"></i> <?php $Strings->get("continue"); ?>
</button>
</div>
</form>

@ -0,0 +1,31 @@
<?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/.
*/
$_SESSION['check'] = "totp";
?>
<form action="" method="POST">
<div class="form-group">
<label for="totp"><?php $Strings->get("Two-factor code"); ?></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-mobile-alt"></i></span>
</div>
<input type="text" class="form-control" id="totp" name="totp" aria-describedby="totpHelp" placeholder="" required autofocus>
</div>
<small id="passwordHelp" class="form-text text-muted">Enter the two-factor code from your mobile device.</small>
</div>
<div class="d-flex">
<a href="./?code=<?php echo htmlentities($_GET['code']); ?>&amp;redirect=<?php echo htmlentities($_GET['redirect']); ?>&amp;reset=1" class="btn btn-link mr-2">
<i class="fas fa-chevron-left"></i> <?php $Strings->get("Back"); ?>
</a>
<button type="submit" class="btn btn-primary ml-auto">
<i class="fas fa-chevron-right"></i> <?php $Strings->get("continue"); ?>
</button>
</div>
</form>

@ -0,0 +1,37 @@
<?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/.
*/
$_SESSION['check'] = "username";
?>
<form action="" method="POST">
<div class="form-group">
<label for="username"><?php $Strings->get("username"); ?></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-user"></i></span>
</div>
<input type="text" class="form-control" id="username" name="username" aria-describedby="usernameHelp" placeholder="" required autofocus>
</div>
<small id="usernameHelp" class="form-text text-muted">Enter your username.</small>
</div>
<div class="d-flex">
<div class="ml-auto">
<?php
if ($SETTINGS['signups_enabled'] === true) {
?>
<a href="../signup/?code=<?php echo urlencode($_GET["code"]); ?>&amp;redirect=<?php echo urlencode($_GET["redirect"]); ?>" class="btn btn-link mr-2"><?php $Strings->get("Create Account"); ?></a>
<?php
}
?>
<button type="submit" class="btn btn-primary">
<i class="fas fa-chevron-right"></i> <?php $Strings->get("continue"); ?>
</button>
</div>
</div>
</form>

@ -18,12 +18,12 @@ if ($VARS['action'] == "ping") {
exit(json_encode(["status" => "OK"]));
}
if (MOBILE_ENABLED !== TRUE) {
if ($SETTINGS['mobile_enabled'] !== TRUE) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("mobile login disabled", false)]));
}
// Make sure we have a username and access key
if (is_empty($VARS['username']) || is_empty($VARS['key'])) {
if (empty($VARS['username']) || empty($VARS['key'])) {
http_response_code(401);
die(json_encode(["status" => "ERROR", "msg" => "Missing username and/or access key."]));
}
@ -32,12 +32,19 @@ $username = strtolower($VARS['username']);
$key = strtoupper($VARS['key']);
// Make sure the username and key are actually legit
$user_key_valid = $database->has('mobile_codes', ['[>]accounts' => ['uid' => 'uid']], ["AND" => ['mobile_codes.code' => $key, 'accounts.username' => $username]]);
if ($user_key_valid !== TRUE) {
engageRateLimit();
//http_response_code(401);
insertAuthLog(21, null, "Username: " . $username . ", Key: " . $key);
die(json_encode(["status" => "ERROR", "msg" => "Invalid username and/or access key."]));
// Don't check key if we're trying to generate one
if ($VARS['action'] == "generatesynccode") {
if (!User::byUsername($username)->exists()) {
Log::insert(LogType::MOBILE_LOGIN_FAILED, null, "Username: " . $username . ", Key: " . $key);
die(json_encode(["status" => "ERROR", "msg" => "Invalid username and/or access key."]));
}
} else {
$user_key_valid = $database->has('mobile_codes', ['[>]accounts' => ['uid' => 'uid']], ["AND" => ['mobile_codes.code' => $key, 'accounts.username' => $username]]);
if ($user_key_valid !== TRUE) {
engageRateLimit();
Log::insert(LogType::MOBILE_BAD_KEY, null, "Username: " . $username . ", Key: " . $key);
die(json_encode(["status" => "ERROR", "msg" => "Invalid username and/or access key."]));
}
}
// Obscure key
@ -99,7 +106,7 @@ switch ($VARS['action']) {
Log::insert(LogType::MOBILE_LOGIN_FAILED, null, "Username: " . $username . ", Key: " . $key);
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
case "listapps":
$apps = EXTERNAL_APPS;
$apps = $SETTINGS['apps'];
// Format paths as absolute URLs
foreach ($apps as $k => $v) {
if (strpos($apps[$k]['url'], "http") === FALSE) {
@ -119,7 +126,119 @@ switch ($VARS['action']) {
$database->delete("onetimekeys", ["expires[<]" => date("Y-m-d H:i:s")]); // cleanup
exit(json_encode(["status" => "OK", "code" => $code]));
case "checknotifications":
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
try {
$notifications = Notifications::get($user, false);
exit(json_encode(["status" => "OK", "notifications" => $notifications]));
} catch (Exception $ex) {
exit(json_encode(["status" => "ERROR", "msg" => $ex->getMessage()]));
}
break;
case "readnotification":
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
if (empty($VARS['id'])) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("invalid parameters", false)]));
}
try {
Notifications::read($user, $VARS['id']);
exit(json_encode(["status" => "OK"]));
} catch (Exception $ex) {
exit(json_encode(["status" => "ERROR", "msg" => $ex->getMessage()]));
}
break;
case "addnotification":
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
try {
$timestamp = "";
if (!empty($VARS['timestamp'])) {
$timestamp = date("Y-m-d H:i:s", strtotime($VARS['timestamp']));
}
$url = "";
if (!empty($VARS['url'])) {
$url = $VARS['url'];
}
$nid = Notifications::add($user, $VARS['title'], $VARS['content'], $timestamp, $url, isset($VARS['sensitive']));
exit(json_encode(["status" => "OK", "id" => $nid]));
} catch (Exception $ex) {
exit(json_encode(["status" => "ERROR", "msg" => $ex->getMessage()]));
}
break;
case "deletenotification":
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
if (empty($VARS['id'])) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("invalid parameters", false)]));
}
try {
Notifications::delete($user, $VARS['id']);
exit(json_encode(["status" => "OK"]));
} catch (Exception $ex) {
exit(json_encode(["status" => "ERROR", "msg" => $ex->getMessage()]));
}
break;
case "hasotp":
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
exit(json_encode(["status" => "OK", "otp" => $user->has2fa()]));
break;
case "generatesynccode":
$user = User::byUsername($username);
if ($user->has2fa()) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("2-factor is enabled, you need to use the QR code or manual setup for security reasons", false)]));
}
if ($user->getStatus()->get() != AccountStatus::NORMAL) {
Log::insert(LogType::MOBILE_LOGIN_FAILED, null, "Username: " . $username . ", Key: " . $key);
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login failed try on web", false)]));
}
if ($user->checkPassword($VARS['password'])) {
Log::insert(LogType::MOBILE_LOGIN_OK, $user->getUID(), "Key: " . $key);
$code = strtoupper(substr(md5(mt_rand() . uniqid("", true)), 0, 20));
$desc = htmlspecialchars($VARS['desc']);
$database->insert('mobile_codes', ['uid' => $user->getUID(), 'code' => $code, 'description' => $desc]);
exit(json_encode(["status" => "OK", "code" => $code]));
} else {
Log::insert(LogType::MOBILE_LOGIN_FAILED, null, "Username: " . $username . ", Key: " . $key);
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
}
default:
http_response_code(404);
die(json_encode(["status" => "ERROR", "msg" => "The requested action is not available."]));
}
}

@ -7,7 +7,7 @@
// List of pages and metadata
define("PAGES", [
"home" => [
"title" => "home",
"title" => "Home",
"navbar" => true,
"icon" => "fas fa-home",
"styles" => [

@ -6,32 +6,9 @@
*/
?>
<div class="d-flex justify-content-center flex-wrap">
<?php
foreach (EXTERNAL_APPS as $a) {
?>
<div class="app-dock-item m-2 mobile-app-hide">
<p class="mb-0">
<a href="<?php echo $a['url']; ?>">
<img class="img-responsive app-icon" src="<?php
if (strpos($a['icon'], "http") !== 0) {
echo $a['url'] . $a['icon'];
} else {
echo $a['icon'];
}
?>"/>
<span class="d-block text-center"><?php echo $a['title']; ?></span>
</a>
</p>
</div>
<?php
}
?>
</div>
<div class="row mt-2">
<?php
foreach (EXTERNAL_APPS as $a) {
foreach ($SETTINGS['apps'] as $a) {
if (!isset($a['card'])) {
continue;
}
@ -69,7 +46,13 @@
</div>
<div class="col-8">
<span class="h5 font-weight-normal"><?php $Strings->get("account security"); ?></span><br />
<?php $Strings->get("Change password, setup 2-factor, and change Station PIN"); ?>
<?php
if ($SETTINGS['station_kiosk']) {
$Strings->get("Change password, setup 2-factor, add app passwords, and change PIN");
} else {
$Strings->get("Change password, setup 2-factor, and add app passwords");
}
?>
</div>
</a>
</div>
@ -84,7 +67,7 @@
</div>
<div class="col-8">
<span class="h5 font-weight-normal"><?php $Strings->get("sync"); ?></span><br />
<?php $Strings->get("Connect mobile devices to AccountHub"); ?>
<?php $Strings->build("Connect mobile devices to {name} and get notifications", ["name" => $SETTINGS['site_title']]); ?>
</div>
</a>
</div>
@ -137,9 +120,9 @@
<?php
$ts = strtotime($n['timestamp']);
if (time() - $ts < 60 * 60 * 12) {
echo date(TIME_FORMAT, $ts);
echo date($SETTINGS['time_format'], $ts);
} else {
echo date(DATETIME_FORMAT, $ts);
echo date($SETTINGS['datetime_format'], $ts);
}
?>
</div>
@ -149,4 +132,4 @@
<?php
}
?>
</div>
</div>

@ -10,12 +10,20 @@ use Endroid\QrCode\ErrorCorrectionLevel;
use Endroid\QrCode\QrCode;
$user = new User($_SESSION['uid']);
if (!empty($_GET['delpass'])) {
if ($database->has("apppasswords", ["AND" => ["uid" => $_SESSION['uid'], "passid" => $_GET['delpass']]])) {
$database->delete("apppasswords", ["AND" => ["uid" => $_SESSION['uid'], "passid" => $_GET['delpass']]]);
}
}
?>
<div class="row justify-content-center">
<div class="col-sm-6 col-lg-4">
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title"><i class="fas fa-key"></i> <?php $Strings->get("change password"); ?></h5>
<hr />
<form action="action.php" method="POST">
<input type="password" class="form-control" name="oldpass" placeholder="<?php $Strings->get("current password"); ?>" />
<input type="password" class="form-control" name="newpass" placeholder="<?php $Strings->get("new password"); ?>" />
@ -28,32 +36,42 @@ $user = new User($_SESSION['uid']);
</div>
</div>
</div>
<div class="col-sm-6 col-lg-4">
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title"><i class="fas fa-th"></i> <?php $Strings->get("change pin"); ?></h5>
<hr />
<?php $Strings->get("pin explanation"); ?>
<hr />
<form action="action.php" method="POST">
<input type="password" class="form-control" name="newpin" placeholder="<?php $Strings->get("new pin"); ?>" maxlength="8" pattern="[0-9]*" inputmode="numeric" />
<input type="password" class="form-control" name="conpin" placeholder="<?php $Strings->get("confirm pin"); ?>" maxlength="8" pattern="[0-9]*" inputmode="numeric" />
<input type="hidden" name="action" value="chpin" />
<input type="hidden" name="source" value="security" />
<br />
<button type="submit" class="btn btn-success btn-block"><?php $Strings->get("change pin"); ?></button>
</form>
<?php
if ($SETTINGS['station_kiosk']) {
?>
<div class="col-sm-6 col-lg-4">
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title"><i class="fas fa-th"></i> <?php $Strings->get("change pin"); ?></h5>
<hr />
<?php $Strings->get("pin explanation"); ?>
<hr />
<form action="action.php" method="POST">
<input type="password" class="form-control" name="newpin" placeholder="<?php $Strings->get("new pin"); ?>" maxlength="8" pattern="[0-9]*" inputmode="numeric" />
<input type="password" class="form-control" name="conpin" placeholder="<?php $Strings->get("confirm pin"); ?>" maxlength="8" pattern="[0-9]*" inputmode="numeric" />
<input type="hidden" name="action" value="chpin" />
<input type="hidden" name="source" value="security" />
<br />
<button type="submit" class="btn btn-success btn-block"><?php $Strings->get("change pin"); ?></button>
</form>
</div>
</div>
</div>
</div>
<?php
}
?>
<div class="col-sm-6 col-lg-4">
<div class="card mb-4">
<div class="card-body">
<div class="card-body pb-0">
<h5 class="card-title"><i class="fas fa-mobile-alt"></i> <?php $Strings->get("setup 2fa"); ?></h5>
<?php
if ($user->has2fa()) {
?>
<hr />
<hr />
</div>
<?php
if ($user->has2fa()) {
?>
<div class="card-body pt-0">
<?php $Strings->get("2fa active") ?>
<hr />
<form action="action.php" method="POST">
@ -61,21 +79,22 @@ $user = new User($_SESSION['uid']);
<input type="hidden" name="source" value="security" />
<button type="submit" class="btn btn-info btn-block"><?php $Strings->get("remove 2fa") ?></button>
</form>
<?php
} else if (!empty($_GET['2fa']) && $_GET['2fa'] == "generate") {
$codeuri = $user->generate2fa();
$label = SYSTEM_NAME . ":" . is_null($user->getEmail()) ? $user->getName() : $user->getEmail();
$issuer = SYSTEM_NAME;
$qrCode = new QrCode($codeuri);
$qrCode->setWriterByName('svg');
$qrCode->setSize(550);
$qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH);
$qrcode = $qrCode->writeDataUri();
$totp = Factory::loadFromProvisioningUri($codeuri);
$codesecret = $totp->getSecret();
$chunk_secret = trim(chunk_split($codesecret, 4, ' '));
?>
</div>
<?php
} else if (!empty($_GET['2fa']) && $_GET['2fa'] == "generate") {
$codeuri = $user->generate2fa();
$label = $SETTINGS['system_name'] . ":" . is_null($user->getEmail()) ? $user->getName() : $user->getEmail();
$issuer = $SETTINGS['system_name'];
$qrCode = new QrCode($codeuri);
$qrCode->setWriterByName('svg');
$qrCode->setSize(550);
$qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH());
$qrcode = $qrCode->writeDataUri();
$totp = Factory::loadFromProvisioningUri($codeuri);
$codesecret = $totp->getSecret();
$chunk_secret = trim(chunk_split($codesecret, 4, ' '));
?>
<div class="card-body pt-0">
<div class="card-text">
<?php $Strings->get("scan 2fa qrcode") ?>
</div>
@ -113,15 +132,99 @@ $user = new User($_SESSION['uid']);
<?php
} else {
?>
<hr />
<?php $Strings->get("2fa explained"); ?>
<hr />
<a class="btn btn-success btn-block" href="app.php?page=security&2fa=generate">
<?php $Strings->get("enable 2fa"); ?>
</a>
<div class="card-body pt-0">
<?php $Strings->get("2fa explained"); ?>
<hr />
<a class="btn btn-success btn-block" href="app.php?page=security&2fa=generate">
<?php $Strings->get("enable 2fa"); ?>
</a>
</div>
<?php
}
?>
</div>
</div>
<div class="col-sm-10 col-md-6 col-lg-4 col-xl-4">
<div class="card mb-4">
<?php
if (!empty($_GET['apppassword']) && $_GET['apppassword'] == "generate" && !empty($_POST['desc'])) {
$code = strtoupper(substr(md5(mt_rand() . uniqid("", true)), 0, 20));
$desc = htmlspecialchars($_POST['desc']);
$chunk_code = str_replace(" ", "-", trim(chunk_split($code, 5, ' ')));
$database->insert('apppasswords', ['uid' => $_SESSION['uid'], 'hash' => password_hash($chunk_code, PASSWORD_DEFAULT), 'description' => $desc]);
?>
<div class="card-body">
<h5 class="card-title"><i class="fas fa-shield-alt"></i> <?php $Strings->get("App Passwords"); ?></h5>
<hr />
<?php $Strings->build("app password setup instructions", ["app_name" => $desc]); ?>
</div>
<div class="list-group list-group-flush">
<div class="list-group-item d-flex justify-content-between align-items-baseline">
<div><?php $Strings->get("username"); ?>:</div>
<div class="text-monospace text-right"><?php echo $_SESSION['username']; ?></div>
</div>
<div class="list-group-item d-flex justify-content-between align-items-baseline">
<div><?php $Strings->get("password"); ?></div>
<div class="text-monospace text-right"><?php echo $chunk_code; ?></div>
</div>
</div>
<div class="card-body">
<a class="btn btn-success btn-block" href="app.php?page=security"><?php $Strings->get("Done"); ?></a>
</div>
<?php
} else {
$activecodes = $database->select("apppasswords", ["passid", "description"], ["uid" => $_SESSION['uid']]);
?>
<div class="card-body">
<h5 class="card-title"><i class="fas fa-shield-alt"></i> <?php $Strings->get("App Passwords"); ?></h5>
<hr />
<p class="card-text">
<?php $Strings->build("app passwords explained", ["site_name" => $SETTINGS['site_title']]); ?>
</p>
<form action="app.php?page=security&apppassword=generate" method="POST">
<input type="text" name="desc" class="form-control" placeholder="<?php $Strings->get("App name"); ?>" required />
<button class="btn btn-success btn-block mt-2" type="submit">
<?php $Strings->get("Generate password"); ?>
</button>
</form>
</div>
<div class="list-group list-group-flush">
<div class="list-group-item">
<b><?php $Strings->get("App Passwords"); ?></b>
</div>
<?php
if (count($activecodes) > 0) {
foreach ($activecodes as $c) {
?>
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<div class="">
<?php echo $c['description']; ?>
</div>
</div>
<div>
<a class="btn btn-danger btn-sm m-1" href="app.php?page=security&delpass=<?php echo $c['passid']; ?>" data-toggle="tooltip" data-placement="bottom" title="<?php $Strings->get("Revoke password"); ?>">
<i class='fas fa-trash'></i><noscript> <?php $Strings->get("Revoke password"); ?></noscript>
</a>
</div>
</div>
<?php
}
} else {
?>
<div class="list-group-item">
<?php $Strings->get("You don't have any app passwords."); ?>
</div>
<?php
}
?>
</div>
<?php
}
?>
</div>
</div>
</div>

@ -15,36 +15,31 @@ if (!empty($_GET['delsynccode'])) {
}
?>
<div class="row justify-content-center">
<div class="col-sm-10 col-md-6 col-lg-5 col-xl-4">
<div class="card">
<div class="col-sm-10 col-md-6 col-lg-4 col-xl-4">
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title"><i class="fas fa-mobile-alt"></i> <?php $Strings->get("sync mobile"); ?></h5>
</div>
<?php
if (!empty($_GET['mobilecode']) && $_GET['mobilecode'] == "generate") {
if (!empty($_GET['showsynccode']) && $database->has("mobile_codes", ["AND" => ["uid" => $_SESSION['uid'], "codeid" => $_GET['showsynccode']]])) {
$code = $database->get("mobile_codes", 'code', ["AND" => ["uid" => $_SESSION['uid'], "codeid" => $_GET['showsynccode']]]);
} else {
<hr />
<?php
if (!empty($_GET['mobilecode']) && $_GET['mobilecode'] == "generate") {
$code = strtoupper(substr(md5(mt_rand() . uniqid("", true)), 0, 20));
$desc = htmlspecialchars($_POST['desc']);
$database->insert('mobile_codes', ['uid' => $_SESSION['uid'], 'code' => $code, 'description' => $desc]);
}
if (strpos(URL, "http") !== FALSE) {
$url = URL . "mobile/index.php";
} else {
$url = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . (($_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) ? ":" . $_SERVER['SERVER_PORT'] : "") . URL . "mobile/index.php";
}
$encodedurl = str_replace("/", "\\", $url);
$codeuri = "bizsync://" . $encodedurl . "/" . $_SESSION['username'] . "/" . $code;
$qrCode = new QrCode($codeuri);
$qrCode->setWriterByName('svg');
$qrCode->setSize(550);
$qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH);
$qrcode = $qrCode->writeDataUri();
$chunk_code = trim(chunk_split($code, 5, ' '));
$lang_done = $Strings->get("done adding sync code", false);
?>
<div class="card-body">
if (strpos($SETTINGS['url'], "http") === 0) {
$url = $SETTINGS['url'] . "mobile/index.php";
} else {
$url = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . (($_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) ? ":" . $_SERVER['SERVER_PORT'] : "") . $SETTINGS['url'] . "mobile/index.php";
}
$encodedurl = str_replace("/", "\\", $url);
$codeuri = "bizsync://" . $encodedurl . "/" . $_SESSION['username'] . "/" . $code;
$qrCode = new QrCode($codeuri);
$qrCode->setWriterByName('svg');
$qrCode->setSize(550);
$qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH);
$qrcode = $qrCode->writeDataUri();
$chunk_code = trim(chunk_split($code, 5, ' '));
$lang_done = $Strings->get("done adding sync code", false);
?>
<p class="card-text"><?php $Strings->get("scan sync qrcode"); ?></p>
</div>
<img src="<?php echo $qrcode; ?>" class="card-img px-4" />
@ -72,57 +67,105 @@ if (!empty($_GET['delsynccode'])) {
} else {
$activecodes = $database->select("mobile_codes", ["codeid", "code", "description"], ["uid" => $_SESSION['uid']]);
?>
<div class="card-body">
<p class="card-text">
<?php $Strings->get("sync explained"); ?>
</p>
<form action="app.php?page=sync&mobilecode=generate" method="POST">
<input type="text" name="desc" class="form-control" placeholder="<?php $Strings->get("sync code name"); ?>" required />
<button class="btn btn-success btn-block mt-2" type="submit">
<?php $Strings->get("generate sync"); ?>
</button>
</form>
<p class="card-text">
<?php $Strings->build("sync explained", ["site_name" => $SETTINGS['site_title']]); ?>
</p>
<form action="app.php?page=sync&mobilecode=generate" method="POST">
<input type="text" name="desc" class="form-control" placeholder="<?php $Strings->get("sync code name"); ?>" required />
<button class="btn btn-success btn-block mt-2" type="submit">
<?php $Strings->get("generate sync"); ?>
</button>
</form>
</div>
<div class="list-group list-group-flush">
<div class="list-group-item">
<b><?php $Strings->get("active sync codes"); ?></b>
</div>
<div class="list-group list-group-flush">
<div class="list-group-item">
<b><?php $Strings->get("active sync codes"); ?></b>
</div>
<?php
if (count($activecodes) > 0) {
foreach ($activecodes as $c) {
?>
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<div class="text-monospace">
<?php echo trim(chunk_split($c['code'], 5, ' ')); ?>
</div>
<div class="text-muted">
<i class="fas fa-mobile-alt"></i> <?php echo $c['description']; ?>
</div>
<?php
if (count($activecodes) > 0) {
foreach ($activecodes as $c) {
// Obscure characters
if (strlen($c['code']) > 7) {
for ($i = 3; $i < strlen($c['code']) - 3; $i++) {
$c['code'][$i] = "*";
}
}
?>
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<div class="">
<?php echo $c['description']; ?>
</div>
<div>
<a class="btn btn-primary btn-sm m-1" href="app.php?page=sync&mobilecode=generate&showsynccode=<?php echo $c['codeid']; ?>">
<i class="fas fa-qrcode"></i>
</a>
<a class="btn btn-danger btn-sm m-1" href="app.php?page=sync&delsynccode=<?php echo $c['codeid']; ?>">
<i class='fas fa-trash'></i>
</a>
<div class="text-muted text-monospace">
<?php echo $c['code']; ?>
</div>
</div>
<?php
}
} else {
?>
<div class="list-group-item">
<?php $Strings->get("no active codes"); ?>
<div>
<a class="btn btn-danger btn-sm m-1" href="app.php?page=sync&delsynccode=<?php echo $c['codeid']; ?>" data-toggle="tooltip" data-placement="bottom" title="<?php $Strings->get("Revoke key"); ?>">
<i class='fas fa-trash'></i><noscript> <?php $Strings->get("Revoke key"); ?></noscript>
</a>
</div>
</div>
<?php
}
} else {
?>
</div>
<div class="list-group-item">
<?php $Strings->get("no active codes"); ?>
</div>
<?php
}
?>
</div>
<?php
}
?>
</div>
</div>
<div class="col-sm-10 col-md-6 col-lg-4 col-xl-4">
<div class="card">
<div class="card-body">
<h5 class="card-title"><i class="fas fa-rss-square"></i> <?php $Strings->get("Notifications"); ?></h5>
<hr />
<p class="card-text">
<?php $Strings->get("notification feed explained"); ?>
</p>
<?php
if ($database->has('userkeys', ['AND' => ['uid' => $_SESSION['uid'], 'typeid' => 1]])) {
$key = $database->get('userkeys', 'key', ['AND' => ['uid' => $_SESSION['uid'], 'typeid' => 1]]);
} else {
$key = RandomString::generate(50);
while ($database->has('userkeys', ['key' => $key])) {
$key = RandomString::generate(50);
}
$database->insert('userkeys', ['uid' => $_SESSION['uid'], 'typeid' => 1, 'created' => date('Y-m-d H:i:s'), 'key' => $key]);
}
if (strpos($SETTINGS['url'], "http") === 0) {
$url = $SETTINGS['url'];
} else {
$url = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . (($_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) ? ":" . $_SERVER['SERVER_PORT'] : "") . $SETTINGS['url'];
}
$url = $url . "feed.php?key=$key";
?>
<a href="<?php echo $url; ?>&type=rss2" target="_BLANK" class="btn btn-orange mr-2"><i class="fas fa-rss"></i> RSS 2.0</a>
<a href="<?php echo $url; ?>&type=rss1" target="_BLANK" class="btn btn-orange mr-2"><i class="fas fa-rss"></i> RSS 1.0</a>
<a href="<?php echo $url; ?>&type=atom" target="_BLANK" class="btn btn-blue"><i class="fas fa-atom"></i> ATOM</a>
<hr />
RSS 2.0: <input type="text" readonly class="form-control" value="<?php echo $url; ?>&type=rss2" />
<br />
RSS 1.0: <input type="text" readonly class="form-control" value="<?php echo $url; ?>&type=rss1" />
<br />
ATOM: <input type="text" readonly class="form-control" value="<?php echo $url; ?>&type=atom" />
<hr />
<form action="action.php" method="POST">
<input type="hidden" name="source" value="sync" />
<input type="hidden" name="action" value="resetfeedkey" />
<button type="submit" class="btn btn-danger"><i class="fas fa-sync"></i> <?php $Strings->get('Reset'); ?></button>
</form>
</div>
</div>
</div>
</div>

@ -8,34 +8,32 @@
* This file contains global settings and utility functions.
*/
ob_start(); // allow sending headers after content
//
// Composer
require __DIR__ . '/vendor/autoload.php';
// Settings file
require __DIR__ . '/settings.php';
// Unicode, solves almost all stupid encoding problems
header('Content-Type: text/html; charset=utf-8');
// l33t $ecurity h4x
// Strip PHP version
header('X-Powered-By: PHP');
// Security
header('X-Content-Type-Options: nosniff');
header('X-XSS-Protection: 1; mode=block');
header('X-Powered-By: PHP'); // no versions makes it harder to find vulns
header('X-Frame-Options: "DENY"');
header('Referrer-Policy: "no-referrer, strict-origin-when-cross-origin"');
$SECURE_NONCE = base64_encode(random_bytes(8));
$session_length = 60 * 60; // 1 hour
$session_length = 60 * 60 * 1; // 1 hour
ini_set('session.gc_maxlifetime', $session_length);
session_set_cookie_params($session_length, "/", null, false, false);
session_start(); // stick some cookies in it
//// renew session cookie
setcookie(session_name(), session_id(), time() + $session_length);
// renew session cookie
setcookie(session_name(), session_id(), time() + $session_length, "/", false, false);
$captcha_server = (CAPTCHA_ENABLED === true ? preg_replace("/http(s)?:\/\//", "", CAPTCHA_SERVER) : "");
if ($_SESSION['mobile'] === TRUE) {
if (isset($_SESSION['mobile']) && $_SESSION['mobile'] === TRUE) {
header("Content-Security-Policy: "
. "default-src 'self';"
. "object-src 'none'; "
@ -45,7 +43,7 @@ if ($_SESSION['mobile'] === TRUE) {
. "font-src 'self'; "
. "connect-src *; "
. "style-src 'self' 'unsafe-inline'; "
. "script-src 'self' 'unsafe-inline' $captcha_server");
. "script-src 'self' 'unsafe-inline'");
} else {
header("Content-Security-Policy: "
. "default-src 'self';"
@ -56,9 +54,13 @@ if ($_SESSION['mobile'] === TRUE) {
. "font-src 'self'; "
. "connect-src *; "
. "style-src 'self' 'nonce-$SECURE_NONCE'; "
. "script-src 'self' 'nonce-$SECURE_NONCE' $captcha_server");
. "script-src 'self' 'nonce-$SECURE_NONCE'");
}
//
// Composer
require __DIR__ . '/vendor/autoload.php';
// List of alert messages
require __DIR__ . '/langs/messages.php';
@ -67,8 +69,12 @@ foreach ($libs as $lib) {
require_once $lib;
}
$Strings = new Strings(LANGUAGE);
$Strings = new Strings($SETTINGS['language']);
/**
* Kill off the running process and spit out an error message
* @param string $error error message
*/
function sendError($error) {
global $SECURE_NONCE;
die("<!DOCTYPE html>"
@ -87,7 +93,7 @@ function sendError($error) {
. "<p>" . htmlspecialchars($error) . "</p>");
}
date_default_timezone_set(TIMEZONE);
date_default_timezone_set($SETTINGS['timezone']);
// Database settings
// Also inits database and stuff
@ -96,12 +102,12 @@ use Medoo\Medoo;
$database;
try {
$database = new Medoo([
'database_type' => DB_TYPE,
'database_name' => DB_NAME,
'server' => DB_SERVER,
'username' => DB_USER,
'password' => DB_PASS,
'charset' => DB_CHARSET
'database_type' => $SETTINGS['database']['type'],
'database_name' => $SETTINGS['database']['name'],
'server' => $SETTINGS['database']['server'],
'username' => $SETTINGS['database']['user'],
'password' => $SETTINGS['database']['password'],
'charset' => $SETTINGS['database']['charset']
]);
} catch (Exception $ex) {
//header('HTTP/1.1 500 Internal Server Error');
@ -109,7 +115,7 @@ try {
}
if (!DEBUG) {
if (!$SETTINGS['debug']) {
error_reporting(0);
} else {
error_reporting(E_ALL);
@ -126,43 +132,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
define("GET", true);
}
/**
* Checks if a string or whatever is empty.
* @param $str The thingy to check
* @return boolean True if it's empty or whatever.
*/
function is_empty($str) {
return (is_null($str) || !isset($str) || $str == '');
}
/**
* Checks if an email address is valid.
* @param string $email Email to check
* @return boolean True if email passes validation, else false.
*/
function isValidEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL);
}
/**
* Hashes the given plaintext password
* @param String $password
* @return String the hash, using bcrypt
*/
function encryptPassword($password) {
return password_hash($password, PASSWORD_BCRYPT);
}
/**
* Securely verify a password and its hash
* @param String $password
* @param String $hash the hash to compare to
* @return boolean True if password OK, else false
*/
function comparePassword($password, $hash) {
return password_verify($password, $hash);
}
function dieifnotloggedin() {
if ($_SESSION['loggedin'] != true) {
sendError("Session expired. Please log out and log in again.");
@ -186,178 +155,14 @@ 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 redirectToPageId($id, $args, $dontdie) {
header('Location: ' . URL . '?id=' . $id . $args);
if (is_null($dontdie)) {
die("Please go to " . URL . '?id=' . $id . $args);
}
}
function redirectIfNotLoggedIn() {
global $SETTINGS;
if ($_SESSION['loggedin'] !== TRUE) {
header('Location: ' . URL . '/login.php');
header('Location: ' . $SETTINGS['url'] . '/index.php');
die();
}
}
/**
* Check if a given ipv4 address is in a given cidr
* @param string $ip IP to check in IPV4 format eg. 127.0.0.1
* @param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed
* @return boolean true if the ip is in this range / false if not.
* @author Thorsten Ott <https://gist.github.com/tott/7684443>
*/
function ip4_in_cidr($ip, $cidr) {
if (strpos($cidr, '/') == false) {
$cidr .= '/32';
}
// $range is in IP/CIDR format eg 127.0.0.1/24
list( $cidr, $netmask ) = explode('/', $cidr, 2);
$range_decimal = ip2long($cidr);
$ip_decimal = ip2long($ip);
$wildcard_decimal = pow(2, ( 32 - $netmask)) - 1;
$netmask_decimal = ~ $wildcard_decimal;
return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
}
/**
* Check if a given ipv6 address is in a given cidr
* @param string $ip IP to check in IPV6 format
* @param string $cidr CIDR netmask
* @return boolean true if the IP is in this range, false otherwise.
* @author MW. <https://stackoverflow.com/a/7952169>
*/
function ip6_in_cidr($ip, $cidr) {
$address = inet_pton($ip);
$subnetAddress = inet_pton(explode("/", $cidr)[0]);
$subnetMask = explode("/", $cidr)[1];
$addr = str_repeat("f", $subnetMask / 4);
switch ($subnetMask % 4) {
case 0:
break;
case 1:
$addr .= "8";
break;
case 2:
$addr .= "c";
break;
case 3:
$addr .= "e";
break;
}
$addr = str_pad($addr, 32, '0');
$addr = pack("H*", $addr);
$binMask = $addr;
return ($address & $binMask) == $subnetAddress;
}
/**
* Check if the REMOTE_ADDR is on Cloudflare's network.
* @return boolean true if it is, otherwise false
*/
function validateCloudflare() {
if (filter_var($_SERVER["REMOTE_ADDR"], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
// Using IPv6
$cloudflare_ips_v6 = [
"2400:cb00::/32",
"2405:8100::/32",
"2405:b500::/32",
"2606:4700::/32",
"2803:f800::/32",
"2c0f:f248::/32",
"2a06:98c0::/29"
];
$valid = false;
foreach ($cloudflare_ips_v6 as $cidr) {
if (ip6_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) {
$valid = true;
break;
}
}
} else {
// Using IPv4
$cloudflare_ips_v4 = [
"103.21.244.0/22",
"103.22.200.0/22",
"103.31.4.0/22",
"104.16.0.0/12",
"108.162.192.0/18",
"131.0.72.0/22",
"141.101.64.0/18",
"162.158.0.0/15",
"172.64.0.0/13",
"173.245.48.0/20",
"188.114.96.0/20",
"190.93.240.0/20",
"197.234.240.0/22",
"198.41.128.0/17"
];
$valid = false;
foreach ($cloudflare_ips_v4 as $cidr) {
if (ip4_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) {
$valid = true;
break;
}
}
}
return $valid;
}
/**
* Makes a good guess at the client's real IP address.
*
* @return string Client IP or `0.0.0.0` if we can't find anything
*/
function getClientIP() {
// If CloudFlare is in the mix, we should use it.
// Check if the request is actually from CloudFlare before trusting it.
if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
if (validateCloudflare()) {
return $_SERVER["HTTP_CF_CONNECTING_IP"];
}
}
if (isset($_SERVER["REMOTE_ADDR"])) {
return $_SERVER["REMOTE_ADDR"];
}
return "0.0.0.0"; // This will not happen unless we aren't a web server
}
/**
* Check if the client's IP has been doing too many brute-force-friendly
* requests lately.
@ -371,12 +176,12 @@ function engageRateLimit() {
global $database;
$delay = date("Y-m-d H:i:s", strtotime("-2 seconds"));
$database->delete('rate_limit', ["lastaction[<]" => $delay]);
if ($database->has('rate_limit', ["AND" => ["ipaddr" => getClientIP()]])) {
if ($database->has('rate_limit', ["AND" => ["ipaddr" => IPUtils::getClientIP()]])) {
http_response_code(429);
// JSONify it so API clients don't scream too loud
die(json_encode(["status" => "ERROR", "msg" => "You're going too fast. Slow down, mkay?"]));
} else {
// Add a record for the IP address
$database->insert('rate_limit', ["ipaddr" => getClientIP(), "lastaction" => date("Y-m-d H:i:s")]);
$database->insert('rate_limit', ["ipaddr" => IPUtils::getClientIP(), "lastaction" => date("Y-m-d H:i:s")]);
}
}

@ -1,169 +1,166 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// Whether to show debugging data in output.
// DO NOT SET TO TRUE IN PRODUCTION!!!
define("DEBUG", false);
// Database connection settings
// See http://medoo.in/api/new for info
define("DB_TYPE", "mysql");
define("DB_NAME", "accounthub");
define("DB_SERVER", "localhost");
define("DB_USER", "accounthub");
define("DB_PASS", "");
define("DB_CHARSET", "utf8");
define("SITE_TITLE", "AccountHub");
// Used to identify the system in OTP and other places
define("SYSTEM_NAME", "Netsyms SSO Demo");
// For supported values, see http://php.net/manual/en/timezones.php
define("TIMEZONE", "America/Denver");
// Allow or prevent users from logging in via the mobile app.
define("MOBILE_ENABLED", TRUE);
// Base URL for site links.
define('URL', 'http://localhost/accounthub');
// Use Captcheck on login screen
// https://captcheck.netsyms.com
define("CAPTCHA_ENABLED", FALSE);
define('CAPTCHA_SERVER', 'https://captcheck.netsyms.com');
// See lang folder for language options
define('LANGUAGE', "en");
// List of available applications, icons, and other info.
// Used in the mobile app and in the "dock" in AccountHub.
define('EXTERNAL_APPS', [
"accounthub" => [
"url" => "/accounthub",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => SITE_TITLE
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
// Settings for the app.
// Copy to settings.php and customize.
$SETTINGS = [
// Whether to output debugging info like PHP notices, warnings,
// and stacktraces.
// Turning this on in production is a security risk and can sometimes break
// things, such as JSON output where extra content is not expected.
"debug" => false,
// Database connection settings
// See http://medoo.in/api/new for info
"database" => [
"type" => "mysql",
"name" => "accounthub",
"server" => "localhost",
"user" => "accounthub",
"password" => "",
"charset" => "utf8"
],
"qwikclock" => [
"url" => "/qwikclock",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => "QwikClock",
"station_features" => [
"qwikclock_punchinout",
"qwikclock_myshifts",
"qwikclock_jobs"
// Name of the app.
"site_title" => "AccountHub",
// Used to identify the system in OTP and other places
"system_name" => "Netsyms AccountHub",
// Allow login from the Netsyms mobile app
"mobile_enabled" => true,
// Allow users to signup for new accounts
"signups_enabled" => false,
// Terms of Service URL for user signup
"tos_url" => "",
// For supported values, see http://php.net/manual/en/timezones.php
"timezone" => "America/Denver",
// List of external apps connected to this system.
// This list is used for generating the dashboard cards and in the
// mobile app.
"apps" => [
"accounthub" => [
"url" => "/accounthub",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => "AccountHub"
],
"card" => [
"color" => "blue",
"string" => "Punch in and check work schedule"
]
],
"binstack" => [
"url" => "/binstack",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => "BinStack",
"card" => [
"color" => "green",
"string" => "Manage physical items"
]
],
"newspen" => [
"url" => "/newspen",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => "NewsPen",
"card" => [
"color" => "purple",
"string" => "Create and publish e-newsletters"
]
],
"managepanel" => [
"url" => "/managepanel",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => "ManagePanel",
"card" => [
"color" => "brown",
"string" => "Manage users, permissions, and security"
]
],
"nickelbox" => [
"url" => "/nickelbox",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => "NickelBox",
"card" => [
"color" => "light-green",
"text" => "dark",
"string" => "Checkout customers and manage online orders"
"qwikclock" => [
"url" => "/qwikclock",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => "QwikClock",
"station_features" => [
"qwikclock_punchinout",
"qwikclock_myshifts",
"qwikclock_jobs"
],
"card" => [
"color" => "blue",
"string" => "Punch in and check work schedule"
]
],
"binstack" => [
"url" => "/binstack",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => "BinStack",
"card" => [
"color" => "green",
"string" => "Manage physical items"
]
],
"newspen" => [
"url" => "/newspen",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => "NewsPen",
"card" => [
"color" => "purple",
"string" => "Create and publish e-newsletters"
]
],
"managepanel" => [
"url" => "/managepanel",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => "ManagePanel",
"card" => [
"color" => "brown",
"string" => "Manage users, permissions, and security"
]
],
"nickelbox" => [
"url" => "/nickelbox",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => "NickelBox",
"card" => [
"color" => "light-green",
"text" => "dark",
"string" => "Checkout customers and manage online orders"
]
],
"sitewriter" => [
"url" => "/sitewriter",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => "SiteWriter",
"card" => [
"color" => "light-blue",
"string" => "Build websites and manage contact form messages"
]
],
"taskfloor" => [
"url" => "/taskfloor",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => "TaskFloor",
"station_features" => [
"taskfloor_viewtasks",
"taskfloor_viewmessages"
],
"card" => [
"color" => "blue-grey",
"string" => "Track jobs and assigned tasks"
]
]
],
"sitewriter" => [
"url" => "/sitewriter",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => "SiteWriter",
"card" => [
"color" => "light-blue",
"string" => "Build websites and manage contact form messages"
]
// Settings for sending emails.
"email" => [
// If false, will use PHP mail() instead of a server
"use_smtp" => true,
// Admin email for alerts
"admin_email" => "",
"from" => "alert-noreply@example.com",
"host" => "",
"auth" => true,
"port" => 587,
"secure" => "tls",
"user" => "",
"password" => "",
"allow_invalid_certificate" => true
],
"taskfloor" => [
"url" => "/taskfloor",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => "TaskFloor",
"station_features" => [
"taskfloor_viewtasks",
"taskfloor_viewmessages"
],
"card" => [
"color" => "blue-grey",
"string" => "Track jobs and assigned tasks"
]
"min_password_length" => 8,
// Show or hide the Station PIN setup option.
"station_kiosk" => true,
// Used for notification timestamp display.
"datetime_format" => "M j, g:i a",
"time_format" => "g:i",
// Use Captcheck on login screen to slow down bots
// https://captcheck.netsyms.com
"captcha" => [
"enabled" => false,
"server" => "https://captcheck.netsyms.com"
],
]);
// Used for notification timestamp display.
define("DATETIME_FORMAT", "M j, g:i a");
define("TIME_FORMAT", "g:i");
// Email settings for receiving admin alerts.
define("USE_SMTP", TRUE); // if FALSE, will use PHP's mail() instead
define("ADMIN_EMAIL", "");
define("FROM_EMAIL", "alert-noreply@apps.biz.netsyms.com");
define("SMTP_HOST", "");
define("SMTP_AUTH", true);
define("SMTP_PORT", 587);
define("SMTP_SECURE", 'tls');
define("SMTP_USER", "");
define("SMTP_PASS", "");
define("SMTP_ALLOW_INVALID_CERTIFICATE", TRUE);
// Minimum length for new passwords
// The system checks new passwords against the 500 worst passwords and rejects
// any matches.
// If you want to have additional password requirements, go edit action.php.
// However, all that does is encourage people to use the infamous
// "post-it password manager". See also https://xkcd.com/936/ and
// http://stackoverflow.com/a/34166252 for reasons why forcing passwords
// like CaPs45$% is not actually a great idea.
// Encourage users to use 2-factor auth whenever possible.
define("MIN_PASSWORD_LENGTH", 8);
// Maximum number of rows to get in a query.
define("QUERY_LIMIT", 1000);
define("FOOTER_TEXT", "");
define("COPYRIGHT_NAME", "Netsyms Technologies");
//////////////////////////////////////////////////////////////
// Language to use for localization. See langs folder to add a language.
"language" => "en",
// Shown in the footer of all the pages.
"footer_text" => "",
// Also shown in the footer, but with "Copyright <current_year>" in front.
"copyright" => "Netsyms Technologies",
// Base URL for building links relative to the location of the app.
"url" => "/accounthub/"
];

@ -14,7 +14,7 @@ if ($database->has('accounts', ["[>]assigned_permissions" => ["uid" => "uid"]],
die("An admin account already exists, exiting.");
}
if (is_empty($_POST['username']) || is_empty($_POST['password']) || is_empty($_POST['realname'])) {
if (empty($_POST['username']) || empty($_POST['password']) || empty($_POST['realname'])) {
?>
<!DOCTYPE html>
<title>Admin Account Creation</title>

@ -0,0 +1,207 @@
<?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';
if ($SETTINGS['signups_enabled'] !== true) {
http_response_code(403);
die("Account creation not allowed. Contact the site administrator for an account.");
}
function showHTML($errormsg = null, $genform = true, $noformcontent = "", $title = null) {
global $SETTINGS, $SECURE_NONCE, $Strings;
try {
$textcaptcha = json_decode(file_get_contents("https://api.textcaptcha.com/netsyms.com.json"));
$captchaquestion = $textcaptcha->q;
$_SESSION["textcaptchaanswers"] = $textcaptcha->a;
} catch (Exception $ex) {
$captchaquestion = "";
}
$form = new FormBuilder("", "", "", "POST");
$form->setID("signupform");
$form->addInput("username", "", "text", true, null, null, "Username", "fas fa-id-card", 6, 4, 100, "[a-zA-Z0-9]+", $Strings->get("Please enter your username (4-100 characters, alphanumeric).", false));
$form->addInput("password", "", "password", true, null, null, "Password", "fas fa-lock", 6, $SETTINGS['min_password_length'], 255, "", $Strings->build("Your password must be at least {n} characters long.", ["n" => $SETTINGS['min_password_length']], false));
$form->addInput("email", "", "email", false, null, null, "Email", "fas fa-envelope", 6, 5, 255, "", $Strings->get("That email address doesn't look right.", false));
$form->addInput("name", "", "text", true, null, null, "Name", "fas fa-user", 6, 2, 200, "", $Strings->get("Enter your name.", false));
if (!empty($captchaquestion)) {
$form->addInput("textcaptcha", "", "text", true, null, null, "$captchaquestion", "fas fa-robot", 12, 1, 200, "", "");
} else {
$form->addHiddenInput("textcaptcha", "DISABLE" . hash("sha1", hash("md5", date("Ymd"))));
}
$form->addHiddenInput("code", empty($_GET["code"]) ? "" : $_GET["code"]);
$form->addHiddenInput("redirect", empty($_GET["redirect"]) ? "" : $_GET["code"]);
if (!empty($SETTINGS['tos_url'])) {
$form->addInput("agree_tos", "1", "checkbox", true, null, null, "I agree to the <a href=\"$SETTINGS[tos_url]\" target=\"_BLANK\">terms of service</a>");
}
$form->addHiddenInput("submit", "1");
$form->addButton($Strings->get("Create Account", false), "fas fa-user-plus", null, "submit", "savebtn");
?>
<!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">
<title><?php echo $SETTINGS['site_title']; ?></title>
<link rel="icon" href="../static/img/logo.svg">
<link href="../static/css/bootstrap.min.css" rel="stylesheet">
<link href="../static/css/svg-with-js.min.css" rel="stylesheet">
<script nonce="<?php echo $SECURE_NONCE; ?>">
FontAwesomeConfig = {autoAddCss: false}
</script>
<style nonce="<?php echo $SECURE_NONCE; ?>">
.display-5 {
font-size: 2.5rem;
font-weight: 300;
line-height: 1.2;
}
.banner-image {
max-height: 100px;
margin: 2em auto;
border: 1px solid grey;
border-radius: 15%;
}
</style>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-12 text-center">
<img class="banner-image" src="../static/img/logo.svg" />
</div>
<div class="col-12 text-center">
<h1 class="display-5 mb-4"><?php
if (is_null($title)) {
$Strings->get("Create Account");
} else {
echo $title;
}
?></h1>
</div>
<div class="col-12 col-sm-8">
<div class="mt-4">
<?php
if (!is_null($errormsg)) {
?>
<div class="alert alert-danger">
<?php echo $errormsg; ?>
</div>
<?php
}
if ($genform) {
$form->generate();
} else {
echo $noformcontent;
}
?>
</div>
</div>
</div>
</div>
<script src="../static/js/fontawesome-all.min.js"></script>
<script src="../static/js/jquery-3.3.1.min.js"></script>
<script nonce="<?php echo $SECURE_NONCE; ?>">
$("#savebtn").click(function (event) {
var form = $("#signupform");
if (form[0].checkValidity() === false) {
event.preventDefault()
event.stopPropagation()
}
form.addClass('was-validated');
});
</script>
<?php
die();
}
// If we didn't submit the form yet
if (empty($_POST['submit'])) {
showHTML();
}
// Validation
if (empty($_POST['username'])) {
showHTML($Strings->get("Choose a username.", false));
}
$_POST['username'] = strtolower($_POST['username']);
if (!preg_match("/^[a-z0-9]+$/", $_POST['username'])) {
showHTML($Strings->get("Please enter your username (4-100 characters, alphanumeric).", false));
}
if (User::byUsername($_POST['username'])->exists()) {
showHTML($Strings->get("Username already taken, pick another.", false));
}
if (empty($_POST['password'])) {
showHTML($Strings->get("Choose a password.", false));
}
if (strlen($_POST['password']) < $SETTINGS['min_password_length']) {
showHTML($Strings->build("Your password must be at least {n} characters long.", ["n" => $SETTINGS[min_password_length]], false));
}
require_once __DIR__ . "/../lib/worst_passwords.php";
$passrank = checkWorst500List($_POST['password']);
if ($passrank !== FALSE) {
showHTML($Strings->get("That password is one of the most popular and insecure ever, make a better one.", false));
}
if (!empty($_POST['email']) && !filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
showHTML($Strings->get("That email address doesn't look right.", false));
}
if (!empty($_POST['email']) && $database->has("accounts", ["email" => strtolower($_POST['email'])])) {
showHTML($Strings->get("That email address is already in use.", false));
}
if (empty($_POST['name'])) {
showHTML($Strings->get("Enter your name.", false));
}
if ($_POST["textcaptcha"] != "DISABLE" . hash("sha1", hash("md5", date("Ymd")))) {
$answer = hash("md5", strtolower($_POST["textcaptcha"]));
$ok = false;
foreach ($_SESSION["textcaptchaanswers"] as $ans) {
if ($ans == $answer) {
$ok = true;
}
}
if (!$ok) {
showHTML($Strings->get("CAPTCHA answer incorrect.", false));
}
}
// Create account
$userid = User::add($_POST['username'], $_POST['password'], $_POST['name'], (filter_var($_POST['email'], FILTER_VALIDATE_EMAIL) ? strtolower($_POST['email']) : null));
$signinstr = $Strings->get("sign in", false);
$redirect = urlencode($_POST["redirect"]);
$code = urlencode($_POST["code"]);
if (!empty($code)) {
showHTML(null, false, <<<END
<div class="card mt-4">
<div class="card-body">
<a href="../login/?code=$code&amp;redirect=$redirect" class="btn btn-primary btn-block">$signinstr</a>
</div>
</div>
END
, $Strings->get("Account Created", false));
} else {
showHTML(null, false, <<<END
<div class="card mt-4">
<div class="card-body">
<a href="../" class="btn btn-primary btn-block">$signinstr</a>
</div>
</div>
END
, $Strings->get("Account Created", false));
}

File diff suppressed because one or more lines are too long

@ -1,5 +0,0 @@
/*!
* Font Awesome Free 5.1.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (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:a 2s infinite linear}.fa-pulse{animation:a 1s infinite steps(8)}@keyframes a{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}

File diff suppressed because one or more lines are too long

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

@ -0,0 +1,23 @@
/*
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/.
*/
.display-5 {
font-size: 2.5rem;
font-weight: 300;
line-height: 1.2;
}
.banner-image {
max-height: 100px;
margin: 2em auto;
border: 1px solid grey;
border-radius: 15%;
}
.blank-image {
height: 100px;
margin: 2em auto;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@ -13,7 +13,7 @@ $(document).ready(function () {
var gone = 20;
var msgticker = setInterval(function () {
if ($('#msg-alert-box .alert:hover').length) {
if ($("#msg-alert-box .alert:hover").length) {
msginteractiontick = 0;
} else {
msginteractiontick++;
@ -55,13 +55,14 @@ $(document).ready(function () {
$("#msg-alert-box").on("mouseenter", function () {
$("#msg-alert-box").css("opacity", "1");
msginteractiontick = 0;
console.log("👈😎👈 zoop");
});
$("#msg-alert-box").on("click", ".close", function (e) {
$("#msg-alert-box").fadeOut("slow");
window.clearInterval(msgticker);
});
}
$('[data-toggle="tooltip"]').tooltip();
});
@ -78,4 +79,4 @@ try {
window.history.replaceState("", "", getniceurl());
} catch (ex) {
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 699 KiB

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save