Compare commits

...

241 Commits

Author SHA1 Message Date
356044fb2d Prevent registrations with same email 2020-07-04 17:07:17 -06:00
368cb6bbbf Add text captcha to signup 2020-07-04 17:00:59 -06:00
14a4fe7c46 Fix signup 2020-07-04 15:37:28 -06:00
2749dfee32 Fix alert emails 2020-07-04 15:30:05 -06:00
033bb50298 Fix alert emails, remove duplicate lang key 2020-07-04 15:26:44 -06:00
7e1439dd3c Merge AppTemplate 2020-05-11 17:36:36 -06:00
26b16ccbe6 Missed a spot 2020-05-11 17:36:26 -06:00
3c6851e38e Update dependencies 2020-05-11 17:32:51 -06:00
bc0665b022 Update dependencies 2020-05-11 17:22:57 -06:00
367062b76c Update FontAwesome version 2020-05-11 17:22:47 -06:00
81ad5e653e Merge ../AppTemplate
# Conflicts:
#	README.md
#	composer.lock
#	nbproject/project.xml
2020-05-11 17:15:01 -06:00
a454dac629 Fix warning message (Business/AccountHub#17) 2020-05-11 17:11:24 -06:00
589364201c Update project title 2020-05-11 17:11:18 -06:00
b36b4080f5 Update dependencies (fix CVE-2018-19296) 2020-03-09 21:33:57 -06:00
ac1ad47aba Add redirect parameters to signup link 2020-03-09 21:33:33 -06:00
bfae187a59 Add API call for adding app password 2019-10-12 23:30:40 -06:00
88cb51f1bb Upgrade Medoo: 1.5 to 1.6 2019-03-29 01:08:46 -06:00
f27812997f Use FontAwesome CSS+webfont instead of JS, upgrade 5.7.2 to 5.8.1 2019-03-29 01:05:34 -06:00
42460d2165 Adjust User.lib.php 2019-03-19 17:34:28 -06:00
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
2019-03-19 17:23:19 -06:00
53e158b553 Update FontAwesome 5.6.0 -> 5.7.2 2019-03-19 17:12:45 -06:00
474047ab34 Update Bootstrap 4.1.3 -> 4.3.1 2019-03-19 17:11:54 -06:00
922ea55cdb Fix #16: add back button to login flow that redirects to username entry 2019-03-19 16:07:24 -06:00
c97e058786 API: Check for user permission 2019-03-01 23:43:31 -07:00
26a662c399 Add addTextInput and addSelect methods that are simpler than addInput 2019-03-01 23:37:36 -07:00
289aaeaa9f Minor text changes 2019-03-01 23:37:06 -07:00
df79def142 Update sync settings card text 2019-02-11 16:32:44 -07:00
de12184bf4 Update security card text 2019-02-11 16:30:32 -07:00
61acc9710b Fix mistake in upgrade SQL 2019-02-11 16:25:11 -07:00
3ca062d995 Enforce app passwords in API for users with two-factor enabled 2019-02-11 16:19:27 -07:00
04702f6090 Check for apppass option in login api 2019-02-11 16:10:09 -07:00
22fb97d0c4 Add app passwords (close #15) 2019-02-11 16:08:56 -07:00
99f2e07f63 Add API key types 2019-02-11 15:21:27 -07:00
29fb7feb85 Add API key type column to apikeys, update database schema 2019-02-11 14:59:33 -07:00
7d30251cd6 Add CORS header to API 2019-01-07 22:18:02 -07:00
7173a50c36 Add textarea to FormBuilder 2019-01-04 17:53:01 -07:00
e66280e07a FormBuilder: add d-flex to footer 2019-01-04 17:29:08 -07:00
3ed75822a1 Update license and readme 2019-01-03 00:13:41 -07:00
7531dc362d Whoops 2019-01-02 23:54:53 -07:00
b250908663 Add more permissions checks 2019-01-02 23:51:47 -07:00
5f7d45e812 Add ToS agree checkbox 2018-12-31 14:23:52 -07:00
0fd1aa2b54 Merge ../BusinessAppTemplate 2018-12-31 14:22:22 -07:00
892102528b Strip tags from aria-label 2018-12-31 14:22:14 -07:00
a89c663ca9 Merge BusinessAppTemplate 2018-12-31 14:17:26 -07:00
69c634ea99 Add checkbox to form builder 2018-12-31 14:14:00 -07:00
a514e66969 Add create account button to login page 2018-12-31 14:00:51 -07:00
5b98d3e00a i18n++ 2018-12-31 13:51:55 -07:00
d853082cdb Better button icon 2018-12-31 13:49:57 -07:00
c7aad627ac Add user self-registration option 2018-12-31 13:48:12 -07:00
6ceeeaa087 Add support for regex matching on API vars 2018-12-27 14:44:10 -07:00
4600c87787 Add logging to new login flow 2018-12-27 13:19:51 -07:00
f1c36fdeb1 Add getRequestUser() function 2018-12-27 00:55:58 -07:00
d36b340692 Make API work with user/pass combo 2018-12-27 00:51:54 -07:00
67388884f0 Move LoginKeys.lib.php to LoginKey.lib.php so the class matches the file 2018-12-27 00:31:11 -07:00
223a431e8b Merge BusinessAppTemplate 2018-12-27 00:30:32 -07:00
d7ca7125ce Nicer access denied message 2018-12-27 00:15:27 -07:00
1729b842ba Add permission check during login 2018-12-26 16:32:43 -07:00
4d2b78bdba Remove unused strings 2018-12-26 16:28:32 -07:00
106e697fc3 Remove captcha-related code, since login is done by AccountHub now 2018-12-26 16:25:48 -07:00
e0802f582b Remove unneeded index.css 2018-12-26 16:22:09 -07:00
93098309cb Fix 2018-12-22 22:56:11 -07:00
6d4144c78d Clear login session variables after success to prevent skipping username entry 2018-12-22 22:55:05 -07:00
51de8283b8 Fix index.php not redirecting to app.php when already logged in 2018-12-22 22:39:08 -07:00
016c71d30d Fix index.php not redirecting to app.php when already logged in 2018-12-22 22:38:50 -07:00
2698fc794e Fix issue with database script and settings template 2018-12-22 21:47:13 -07:00
ba1369d842 Add app icon to login flow 2018-12-22 21:26:57 -07:00
2836a05f90 Add app icon to login flow 2018-12-22 21:26:30 -07:00
27502ed710 Fix unclosed div tag 2018-12-22 21:08:22 -07:00
3d3e975519 Fix logout redirect 2018-12-22 21:07:52 -07:00
a559901ac0 Redirect to AccountHub for user login 2018-12-22 16:57:45 -07:00
74971a4592 Add new more flexible login flow 2018-12-22 16:56:25 -07:00
16be9438b9 Minor text fixes 2018-12-21 01:17:18 -07:00
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
2018-12-21 01:16:29 -07:00
3f32258ba0 Fix bug 2018-12-20 23:58:35 -07:00
129efd13c7 Add documentation comments to settings 2018-12-20 23:54:25 -07:00
c179ed7ebb Make settings.php an array, not a bunch of defines 2018-12-20 23:45:45 -07:00
f1a85f47fd Add comment 2018-12-20 23:25:34 -07:00
61d660be69 Add FormBuilder 2018-12-20 23:24:47 -07:00
20a6c6f143 Fix bug 2018-12-15 04:11:16 -07:00
5b7ab65946 Make better API system, use new AccountHub API 2018-12-14 21:16:31 -07:00
fb25c4395a Make better API system 2018-12-14 20:34:04 -07:00
13b60de915 Remove is_empty() 2018-12-14 19:14:22 -07:00
0e094809fa Cleanup some technical debt 2018-12-14 19:13:26 -07:00
ca179b89ea Add option to hide Station PIN settings, update branding 2018-12-14 15:04:26 -07:00
32cd18933d Update FontAwesome from 5.3.1 to 5.6.0 2018-12-11 21:39:25 -07:00
4f1b81ff4b Deprecate is_empty() 2018-12-04 19:48:23 -07:00
cb3c8aaf2d Support undefined messages 2018-12-04 19:46:08 -07:00
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
2018-12-02 13:51:27 -07:00
b55eaea821 Improve noscript UX 2018-11-28 23:19:47 -07:00
1d81bfb83d Remove app bar 2018-11-28 23:18:13 -07:00
ec44a6740f Fix "language key ... is defined more than once" warning 2018-11-28 22:50:29 -07:00
e714286e5a Close #13 2018-11-24 00:59:50 -07:00
bcc41b887d Update database.sql 2018-11-09 15:26:44 -07:00
eee5af3081 Add generatesynccode mobile API (Business/MobileApp/issues/15) 2018-11-08 21:19:43 -07:00
47539de2d7 Fix bootstrap font URL 2018-09-25 13:07:54 -06:00
12aea4a2e2 Update Bootstrap (https://github.com/thomaspark/bootswatch/issues/861) 2018-09-25 12:54:45 -06:00
4c135d6e59 Update composer, adjust margin 2018-09-22 23:00:20 -06:00
34f49bfd01 Update README.md 2018-09-22 22:46:43 -06:00
d4621de80f Update README.md 2018-09-22 22:43:56 -06:00
39ccaa2f2d Add RSS/ATOM notification feeds, close #12 2018-09-22 22:08:54 -06:00
f43f986e25 Adjust cards 2018-09-22 21:08:28 -06:00
c36c365a1b Update Bootstrap and FontAwesome 2018-09-21 16:38:34 -06:00
8dd7ee6005 Update Bootstrap and FontAwesome 2018-09-21 16:36:29 -06:00
80d0a017ed Update Bootstrap to 4.1.3 2018-09-07 15:11:43 -06:00
a17f51b72d Update FontAwesome 5.1.0 to 5.3.1 2018-09-07 15:09:04 -06:00
1271317eb9 Rewrite to use classes, aligning with AccountHub 2.0 2018-09-07 15:03:42 -06:00
2caec48e4c Add mobile notification API calls 2018-08-07 01:43:27 -06:00
0c7b4a31f1 Add "all caught up" message when no notifications to show 2018-07-28 16:07:02 -06:00
1a01f67662 Add appid column to notifications table, remove unused code 2018-07-28 15:56:44 -06:00
3a9344c535 Update installation instructions, add upgrade instructions (issue #11) 2018-07-26 09:49:59 -06:00
c603644c26 Update database.sql from model 2018-07-24 01:23:46 -06:00
a82e4ba363 Add notification view/delete UI to homepage (closes #10) 2018-07-24 01:16:27 -06:00
29bc479355 Sort results of Notifications::get() by seen status and timestamp 2018-07-24 00:53:10 -06:00
e1970a8636 Adjust homepage 2018-07-23 21:34:16 -06:00
e321b84434 Add cards for security and sync pages 2018-07-23 21:29:59 -06:00
bc7d7cd313 Add bottom margin on cards 2018-07-23 21:13:47 -06:00
92162bd1dc Improve app dock/bar 2018-07-23 21:11:20 -06:00
a41b6a689e Add app cards to home page 2018-07-23 21:02:44 -06:00
2c0819d8b7 Fix setup.php 2018-07-23 20:08:04 -06:00
c6e0e1913f Create Notifications class 2018-07-12 02:19:06 -06:00
5374fa0611 Add notification API, close #7 2018-07-12 00:00:38 -06:00
0c70bb25ed Clean up settings.template.php 2018-07-11 23:36:50 -06:00
4cbac22bea Remove LDAP, close #5 2018-07-11 23:35:16 -06:00
b23e4bce30 Rewrite a lot of code, close Business/CommonBugs#3, close Business/CommonBugs#4, close #9 2018-07-11 23:32:47 -06:00
3763d0d485 Remove lang() and lang2() and rewrite references 2018-07-09 23:16:43 -06:00
deca0d330d Remove home screen widgets, build new i18n system, close #4 2018-07-09 23:12:19 -06:00
963fbfbf00 Upgrade FontAwesome to 5.1.0 2018-06-29 14:58:54 -06:00
10575f6f59 Fix another visual bug 2018-06-01 14:28:30 -06:00
2f9eccf931 Fix bug where clicking close on msg alert didn't remove progress bar 2018-06-01 14:23:14 -06:00
769d24b4b7 Improve alert fadeout 2018-06-01 14:04:04 -06:00
66aa3d6fdc Make msg alert time out with smooth visual effects 2018-06-01 13:59:31 -06:00
ee0c0f65e3 Fix PHP variable warnings 2018-05-26 20:51:13 -06:00
66fa86e04e Change string 2018-05-24 19:53:26 -06:00
dafc3b76ea Add message if user is kicked out of application for lack of permissions 2018-05-24 19:52:21 -06:00
8e65d4c98d Use bundled Roboto font 2018-05-21 18:26:25 -06:00
3d309ac68b Update database schema 2018-05-21 16:07:34 -06:00
9dd9f9297c Fix a few undefined variable notices 2018-05-21 15:48:19 -06:00
41c8b6c16b Fix #2 2018-05-17 21:44:53 -06:00
be34857d71 Change session ID on successful login, make sessions last at least 2 hours 2018-05-15 13:44:43 -06:00
58a991cbd0 Update FontAwesome to 5.0.13 2018-05-13 00:29:09 -06:00
023480bf88 Use SVG logo for login page 2018-05-06 22:18:32 -06:00
4bab466169 Fix #1 2018-05-05 12:21:26 -06:00
d4070b36b9 Update FontAwesome 5.0.10 => 5.0.12, update Materia Bootstrap 2018-05-04 18:17:49 -06:00
2d98c68efd Remove unused old arrow icons 2018-04-16 14:21:46 -06:00
2461fec102 Update FontAwesome to 5.0.10 2018-04-16 14:21:23 -06:00
fa6924eb08 Update bootstrap.min.css with upstream bugfix from Bootswatch (https://github.com/thomaspark/bootswatch/issues/819) 2018-04-16 14:18:33 -06:00
dab01a21c4 Update composer packages 2018-04-15 20:39:35 -06:00
a0d2293a3d Use captcheck.dist.js 2018-04-15 20:16:37 -06:00
606b286b1e Switch from reCAPTCHA to Captcheck 2018-04-15 20:07:01 -06:00
a9eb59c936 Replace reCAPTCHA with Captcheck 2018-04-15 19:28:34 -06:00
64c3d47c32 Add Roboto font files 2018-04-13 01:42:02 -06:00
6a7ea5eeb7 Upgrade to Bootstrap 4.1 2018-04-13 01:38:41 -06:00
ca6e1f2c5a Update README 2018-04-10 01:30:32 -06:00
038a712b88 Update README 2018-04-10 01:29:53 -06:00
64add57446 Add MPL header 2018-04-09 19:22:01 -06:00
d10c6214a6 Switch to Mozilla Public License 2.0 for code consistency 2018-04-09 19:18:19 -06:00
2a0d5bc92b Add some more preload headers 2018-04-08 20:07:03 -06:00
fea9e372c8 Update license year 2018-04-08 16:53:34 -06:00
d193f3df4a Update license 2018-04-08 16:45:00 -06:00
814c0dbc0f Fix URL 2018-04-08 16:37:46 -06:00
386615976e Remove test page 2018-04-08 16:37:33 -06:00
35e531a56b Add link preload headers 2018-04-08 16:08:08 -06:00
644e5c2e37 Add <link rel="icon"> 2018-02-16 14:36:03 -07:00
25f2f7df65 I didn't change anything, but Git says I did ¯\_(ツ)_/¯ 2018-02-15 17:04:52 -07:00
0691dd51f1 Remove unused CSS 2018-01-30 22:33:17 -07:00
8441008219 Remove unneeded font files 2018-01-28 23:55:19 -07:00
e155ebe165 Update material-color submodule 2018-01-28 21:06:20 -07:00
c7c7e4e4ea Hide brand-icon on smaller screens 2018-01-27 22:38:33 -07:00
ee47026c0e Adjust alerts 2018-01-27 22:06:53 -07:00
eefa7ab00f Improve mobile app compatibility 2018-01-27 22:04:01 -07:00
121a49e9e0 Upgrade to Bootstrap 4.0, FontAwesome 5.0, and jQuery 3.3.1 2018-01-27 19:18:38 -07:00
8d0a0866b1 getusersbygroup now sorts alphabetically 2018-01-19 14:19:33 -07:00
a5b18b8ab9 Fix URLs 2018-01-17 21:02:15 -07:00
8c13c19b9b Update sample config 2018-01-06 22:58:28 -07:00
ce8e0fb4e3 Improve isManagerOf() error handling to prevent possible security bug 2018-01-03 21:55:48 -07:00
f4ab62c4ff Fix ismanagerof API not detecting "uid" flag properly 2018-01-03 21:53:42 -07:00
87c01fe2ce Move sync settings to separate page 2018-01-03 21:12:04 -07:00
abb306a36e Collapse navbar and iconify right-side navbar items on small screens 2018-01-03 20:57:03 -07:00
2e3cfb9546 Iconify right-side navbar items on small screens 2018-01-03 20:28:09 -07:00
ec2fac4ee4 Add Change PIN tool 2017-12-30 14:29:20 -07:00
5ed3420173 Add i18n to change_password 2017-12-30 12:53:12 -07:00
cb6a1c729c Add checkpin API, add pin boolean to userinfo and getusersbygroup 2017-12-30 11:30:48 -07:00
d3bc37b40f Add pin column to accounts table 2017-12-30 11:17:24 -07:00
d54ebed189 Update dependencies 2017-12-28 16:20:45 -07:00
55f3141a07 Update dependencies 2017-12-28 16:12:15 -07:00
4ac39bd0d3 Add basic group info APIs 2017-12-21 01:21:18 -07:00
7c44b18854 Add checkAPIKey($key) function to login.php 2017-12-20 17:37:40 -07:00
29f4f9e9d3 Add station feature info to app list 2017-12-20 17:37:00 -07:00
627b7f9278 Add "listapps" API 2017-12-20 17:36:25 -07:00
e5294bbecd Add mobile code login for Station client 2017-12-18 01:44:53 -07:00
b505a74502 s/LICENSE_TEXT/FOOTER_TEXT 2017-12-16 13:48:24 -07:00
37789df696 Trigger warning with missing lang strings 2017-12-01 22:06:04 -07:00
5277c5e0fb composer update 2017-11-19 01:32:07 -07:00
5dae7bc168 Refactor and enforce Content-Security-Policy 2017-11-13 16:14:40 -07:00
a04207da62 Update cookie settings 2017-11-08 02:35:12 -07:00
c0a93fb666 Convert material-color CSS to submodule 2017-11-07 15:21:14 -07:00
496b213a88 Fix Business/CommonBugs#1 (expired session logout error) 2017-10-10 12:34:36 -06:00
bf76d3733c Optimize icons 2017-08-09 12:29:07 -06:00
8d2ac32419 Update settings template for Portal->AccountHub branding 2017-07-21 00:59:09 -06:00
d229aee8d0 Nothing 2017-07-10 13:53:49 -06:00
5588ee494d Improve mobile integration, add autocorrect etc. flags to login fields 2017-07-03 03:13:29 -06:00
2770e96a8a Move menu button around on mobile 2017-07-01 00:16:47 -06:00
3d01bf8feb Remove debug data 2017-06-29 04:10:41 -06:00
0405f695f3 Add mobile API 2017-06-29 04:09:47 -06:00
4ad42bfe48 Add sendLoginAlertEmail() code 2017-06-23 15:49:49 -06:00
0c85643847 Update composer packages 2017-05-30 15:27:40 -06:00
d918af5d65 Add account_has_permission function 2017-05-26 00:05:59 -06:00
89c6c720f1 Add material-color to index.php 2017-05-20 20:00:08 -06:00
542bb80d85 Update dependencies 2017-05-20 01:06:35 -06:00
2284117cfc add JS to remove &msg=xyz from URL, update material-color to v1.2 2017-05-15 11:48:24 -06:00
b9f385d6f0 Update material-color.css to version 1.1 2017-05-14 22:12:18 -06:00
3ab33d19f6 Add material-color.css 2017-05-14 19:32:43 -06:00
bdcb7e263d Add Material Design arrow icon to replace app icon when not on the homepage 2017-05-14 19:05:23 -06:00
47f51420fa Add border on app icon when shown large 2017-05-14 19:05:15 -06:00
0b811feccb Add api.php 2017-05-14 11:59:52 -06:00
e8c9cd56e2 Remove unused config variables, remove uncompressed CSS 2017-05-13 22:29:22 -06:00
d6df9d582c User name in navbar now links to Portal home 2017-05-13 16:39:16 -06:00
455a199d78 Update README 2017-05-07 00:50:16 -06:00
ab53d719da Update README 2017-05-07 00:47:53 -06:00
2f31066a0c Add lib/userinfo.php 2017-05-07 00:47:43 -06:00
ea71b78169 Update README 2017-05-07 00:35:12 -06:00
427390cbc4 Add a bit of cleanup and comments 2017-05-07 00:30:25 -06:00
f14393a8a4 Add readme link to Portal API docs 2017-05-07 00:16:57 -06:00
bae6d1ac17 Add renew session cookie on page load 2017-05-07 00:14:59 -06:00
de4dcc37bc Add uid_exists($uid), better login error messages 2017-05-06 23:20:18 -06:00
eaeb8806a1 Add navbar color theme CSS options 2017-05-05 17:15:32 -06:00
8b9407c274 Pages can now define extra stylesheet and script URLs 2017-05-04 00:14:08 -06:00
501f127f04 Disable autocomplete on auth code input 2017-05-03 13:31:15 -06:00
dcd495f4e4 Add reCAPTCHA support, fix bug that allowed logins with only a username and 2fa code 2017-05-02 19:17:59 -06:00
c6941c7bd3 Remove unused variable reference 2017-05-01 13:13:16 -06:00
17c587d3b2 Update readme 2017-04-26 00:57:54 -06:00
ef6bddddeb Update readme 2017-04-26 00:57:06 -06:00
591b5e6ff1 Update readme 2017-04-26 00:56:31 -06:00
8afe41070b Update readme 2017-04-26 00:55:34 -06:00
292ce29b31 Update readme 2017-04-26 00:54:01 -06:00
71347c33f1 Update readme 2017-04-26 00:51:59 -06:00
35df787547 Add LICENSE 2017-04-26 00:47:19 -06:00
ba5ba051e9 Add readme 2017-04-26 00:46:49 -06:00
e28d3a93ac Clean up clutter and unneeded code 2017-04-26 00:46:36 -06:00
3110011596 Add Portal API integration, add icon/style settings, add navbar and icon
options to PAGES
2017-04-25 18:22:27 -06:00
16cbf2a5f1 Create template based on SSO code 2017-04-25 12:09:06 -06:00
173 changed files with 16544 additions and 5875 deletions

2
.gitignore vendored
View File

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

View File

@ -1,43 +1,31 @@
Copyright (c) 2017 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
==================================
### 1. Definitions
**1.1. “Contributor”**
**1.1. “Contributor”**
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
**1.2. “Contributor Version”**
**1.2. “Contributor Version”**
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
**1.3. “Contribution”**
**1.3. “Contribution”**
means Covered Software of a particular Contributor.
**1.4. “Covered Software”**
**1.4. “Covered Software”**
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
**1.5. “Incompatible With Secondary Licenses”**
**1.5. “Incompatible With Secondary Licenses”**
means
* **(a)** that the initial Contributor has attached the notice described
@ -46,22 +34,22 @@ Mozilla Public License Version 2.0
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
**1.6. “Executable Form”**
**1.6. “Executable Form”**
means any form of the work other than Source Code Form.
**1.7. “Larger Work”**
means a work that combines Covered Software with other material, in
**1.7. “Larger Work”**
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
**1.8. “License”**
**1.8. “License”**
means this document.
**1.9. “Licensable”**
**1.9. “Licensable”**
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
**1.10. “Modifications”**
**1.10. “Modifications”**
means any of the following:
* **(a)** any file in Source Code Form that results from an addition to,
@ -70,7 +58,7 @@ Mozilla Public License Version 2.0
* **(b)** any new file in Source Code Form that contains any Covered
Software.
**1.11. “Patent Claims” of a Contributor**
**1.11. “Patent Claims” of a Contributor**
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
@ -78,16 +66,16 @@ Mozilla Public License Version 2.0
made, import, or transfer of either its Contributions or its
Contributor Version.
**1.12. “Secondary License”**
**1.12. “Secondary License”**
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
**1.13. “Source Code Form”**
**1.13. “Source Code Form”**
means the form of the work preferred for making modifications.
**1.14. “You” (or “Your”)**
**1.14. “You” (or “Your”)**
means an individual or a legal entity exercising rights under this
License. For legal entities, “You” includes any entity that
controls, is controlled by, or is under common control with You. For

View File

@ -10,19 +10,24 @@ https://netsyms.biz/apps/accounthub
Installing
----------
*We're working on a nice script to install it automatically, but until then...*
0. Setup a LAMP server with additional PHP extensions mbstring, zip, ldap, gd, imagick
0. Setup a LAMP server with PHP 7.2, including PHP extensions mbstring, zip, gd, and imagick
1. Copy `settings.template.php` to `settings.php`
2. Import `database.sql` into your database server
3. Edit `settings.php` and fill in your DB info
4. Setup LDAP auth, or set "LDAP_ENABLED" to FALSE
5. Set the URL of the install
6. Set the API and HOME values for TaskFloor, Inventory (BinStack), QwikClock, and in the "EXTERNAL_APPS" setting
7. Remove any apps you aren't installing from "EXTERNAL_APPS"
8. Setup the email settings to receive alerts you configure later in ManagePanel
9. Run `composer install` (or `composer.phar install`) to install dependency libraries
10. Edit the database table `apikeys` and add some API keys for the other apps to use
11. From a web browser, visit `http://apps/url` (or whatever your setup is). If you did everything right, you should see a login screen.
12. Now go to `http://apps/url/setup.php` and create an admin account.
13. Install [ManagePanel](https://source.netsyms.com/Business/ManagePanel) to setup additional user accounts.
4. Set the URL of the install
5. Setup "EXTERNAL_APPS" with specifics for your install.
6. Setup the email settings to receive alerts you configure later in ManagePanel
7. Run `composer install` (or `composer.phar install`) to install dependency libraries
8. Edit the database table `apikeys` and add some API keys for the other apps to use
9. From a web browser, visit `http://apps/url` (or whatever your setup is). If you did everything right, you should see a login screen.
10. Now go to `http://apps/url/setup.php` and create an admin account.
11. Install [ManagePanel](https://source.netsyms.com/Business/ManagePanel) to setup additional user accounts.
Upgrading
---------
1. Run `git pull` or otherwise update the code
2. Run `composer install` to update dependencies
3. Execute the SQL scripts in `database_upgrade` to take you from your current version to the latest version
4. Rewrite your `settings.php` based on the new `settings.template.php`

View File

@ -21,55 +21,100 @@ if ($VARS['action'] == 'signout' && $_SESSION['loggedin'] != true) {
dieifnotloggedin();
engageRateLimit();
require_once __DIR__ . "/lib/login.php";
function returnToSender($msg, $arg = "") {
global $VARS;
if ($arg == "") {
header("Location: home.php?page=" . urlencode($VARS['source']) . "&msg=$msg");
} else {
header("Location: home.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();
}
switch ($VARS['action']) {
case "signout":
insertAuthLog(11, $_SESSION['uid']);
Log::insert(LogType::LOGOUT, $_SESSION['uid']);
session_destroy();
header('Location: index.php');
header('Location: index.php?logout=1');
die("Logged out.");
case "chpasswd":
engageRateLimit();
$error = [];
$result = change_password($VARS['oldpass'], $VARS['newpass'], $VARS['conpass'], $error);
if ($result === TRUE) {
returnToSender("password_updated");
}
switch (count($error)) {
case 1:
returnToSender($error[0]);
case 2:
returnToSender($error[0], $error[1]);
default:
returnToSender("generic_op_error");
$user = new User($_SESSION['uid']);
try {
$result = $user->changePassword($VARS['oldpass'], $VARS['newpass'], $VARS['conpass']);
if ($result === TRUE) {
returnToSender("password_updated");
}
} catch (PasswordMatchException $e) {
returnToSender("passwords_same");
} catch (PasswordMismatchException $e) {
returnToSender("new_password_mismatch");
} catch (IncorrectPasswordException $e) {
returnToSender("old_password_mismatch");
} catch (WeakPasswordException $e) {
returnToSender("weak_password");
}
break;
case "chpin":
engageRateLimit();
$error = [];
if (!($VARS['newpin'] == "" || (is_numeric($VARS['newpin']) && strlen($VARS['newpin']) >= 1 && strlen($VARS['newpin']) <= 8))) {
returnToSender("invalid_pin_format");
}
if ($VARS['newpin'] == $VARS['conpin']) {
$database->update('accounts', ['pin' => ($VARS['newpin'] == "" ? null : $VARS['newpin'])], ['uid' => $_SESSION['uid']]);
returnToSender("pin_updated");
}
returnToSender("new_pin_mismatch");
break;
case "add2fa":
if (is_empty($VARS['secret'])) {
if (empty($VARS['secret'])) {
returnToSender("invalid_parameters");
}
$user = new User($_SESSION['uid']);
$totp = new TOTP(null, $VARS['secret']);
if (!$totp->verify($VARS["totpcode"])) {
returnToSender("2fa_wrong_code");
}
$database->update('accounts', ['authsecret' => $VARS['secret']], ['uid' => $_SESSION['uid']]);
insertAuthLog(9, $_SESSION['uid']);
$user->save2fa($VARS['secret']);
Log::insert(LogType::ADDED_2FA, $user);
returnToSender("2fa_enabled");
case "rm2fa":
$database->update('accounts', ['authsecret' => ""], ['uid' => $_SESSION['uid']]);
insertAuthLog(10, $_SESSION['uid']);
engageRateLimit();
(new User($_SESSION['uid']))->save2fa("");
Log::insert(LogType::REMOVED_2FA, $_SESSION['uid']);
returnToSender("2fa_removed");
break;
}
case "readnotification":
$user = new User($_SESSION['uid']);
if (empty($VARS['id'])) {
returnToSender("invalid_parameters#notifications");
}
try {
Notifications::read($user, $VARS['id']);
returnToSender("#notifications");
} catch (Exception $ex) {
returnToSender("invalid_parameters#notifications");
}
break;
case "deletenotification":
$user = new User($_SESSION['uid']);
if (empty($VARS['id'])) {
returnToSender("invalid_parameters#notifications");
}
try {
Notifications::delete($user, $VARS['id']);
returnToSender("notification_deleted#notifications");
} catch (Exception $ex) {
returnToSender("invalid_parameters#notifications");
}
break;
case "resetfeedkey":
$database->delete('userkeys', ['AND' => ['uid' => $_SESSION['uid'], 'typeid' => 1]]);
returnToSender("feed_key_reset");
break;
}

293
api.php
View File

@ -4,295 +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';
require_once __DIR__ . '/lib/login.php';
header("Content-Type: application/json");
//try {
$key = $VARS['key'];
if ($database->has('apikeys', ['key' => $key]) !== TRUE) {
engageRateLimit();
http_response_code(403);
insertAuthLog(14, 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;
}
switch ($VARS['action']) {
case "ping":
exit(json_encode(["status" => "OK"]));
break;
case "auth":
$errmsg = "";
if (authenticate_user($VARS['username'], $VARS['password'], $errmsg)) {
insertAuthLog(12, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "OK", "msg" => lang("login successful", false)]));
} else {
insertAuthLog(13, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
if (!is_empty($errmsg)) {
exit(json_encode(["status" => "ERROR", "msg" => lang2("ldap error", ['error' => $errmsg], false)]));
}
if (user_exists($VARS['username'])) {
switch (get_account_status($VARS['username'])) {
case "LOCKED_OR_DISABLED":
exit(json_encode(["status" => "ERROR", "msg" => lang("account locked", false)]));
case "TERMINATED":
exit(json_encode(["status" => "ERROR", "msg" => lang("account terminated", false)]));
case "CHANGE_PASSWORD":
exit(json_encode(["status" => "ERROR", "msg" => lang("password expired", false)]));
case "NORMAL":
break;
default:
exit(json_encode(["status" => "ERROR", "msg" => lang("account state error", false)]));
}
}
exit(json_encode(["status" => "ERROR", "msg" => lang("login incorrect", false)]));
}
break;
case "userinfo":
if (!is_empty($VARS['username'])) {
if (user_exists_local($VARS['username'])) {
$data = $database->select("accounts", ["uid", "username", "realname (name)", "email", "phone" => ["phone1 (1)", "phone2 (2)"]], ["username" => strtolower($VARS['username'])])[0];
exit(json_encode(["status" => "OK", "data" => $data]));
} else {
exit(json_encode(["status" => "ERROR", "msg" => lang("login incorrect", false)]));
}
} else if (!is_empty($VARS['uid'])) {
if ($database->has('accounts', ['uid' => $VARS['uid']])) {
$data = $database->select("accounts", ["uid", "username", "realname (name)", "email", "phone" => ["phone1 (1)", "phone2 (2)"]], ["uid" => $VARS['uid']])[0];
exit(json_encode(["status" => "OK", "data" => $data]));
} else {
exit(json_encode(["status" => "ERROR", "msg" => lang("login incorrect", false)]));
}
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
break;
case "userexists":
if (!is_empty($VARS['uid'])) {
if ($database->has('accounts', ['uid' => $VARS['uid']])) {
exit(json_encode(["status" => "OK", "exists" => true]));
} else {
exit(json_encode(["status" => "OK", "exists" => false]));
}
}
if (user_exists_local($VARS['username'])) {
exit(json_encode(["status" => "OK", "exists" => true]));
} else {
exit(json_encode(["status" => "OK", "exists" => false]));
}
break;
case "hastotp":
if (userHasTOTP($VARS['username'])) {
exit(json_encode(["status" => "OK", "otp" => true]));
} else {
exit(json_encode(["status" => "OK", "otp" => false]));
}
break;
case "verifytotp":
if (verifyTOTP($VARS['username'], $VARS['code'])) {
exit(json_encode(["status" => "OK", "valid" => true]));
} else {
insertAuthLog(7, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "ERROR", "msg" => lang("2fa incorrect", false), "valid" => false]));
}
break;
case "acctstatus":
exit(json_encode(["status" => "OK", "account" => get_account_status($VARS['username'])]));
case "login":
engageRateLimit();
// simulate a login, checking account status and alerts
$errmsg = "";
if (authenticate_user($VARS['username'], $VARS['password'], $errmsg)) {
$uid = $database->select('accounts', 'uid', ['username' => strtolower($VARS['username'])])[0];
switch (get_account_status($VARS['username'])) {
case "LOCKED_OR_DISABLED":
insertAuthLog(5, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "ERROR", "msg" => lang("account locked", false)]));
case "TERMINATED":
insertAuthLog(5, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "ERROR", "msg" => lang("account terminated", false)]));
case "CHANGE_PASSWORD":
insertAuthLog(5, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "ERROR", "msg" => lang("password expired", false)]));
case "NORMAL":
insertAuthLog(4, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "OK"]));
case "ALERT_ON_ACCESS":
sendLoginAlertEmail($VARS['username']);
insertAuthLog(4, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "OK", "alert" => true]));
default:
insertAuthLog(5, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "ERROR", "msg" => lang("account state error", false)]));
}
} else {
insertAuthLog(5, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
if (!is_empty($errmsg)) {
exit(json_encode(["status" => "ERROR", "msg" => lang2("ldap error", ['error' => $errmsg], false)]));
}
exit(json_encode(["status" => "ERROR", "msg" => lang("login incorrect", false)]));
}
break;
case "ismanagerof":
if ($VARS['uid'] === 1) {
if ($database->has("accounts", ['uid' => $VARS['manager']])) {
if ($database->has("accounts", ['uid' => $VARS['employee']])) {
$managerid = $VARS['manager'];
$employeeid = $VARS['employee'];
} else {
exit(json_encode(["status" => "ERROR", "msg" => lang("user does not exist", false), "user" => $VARS['employee']]));
}
} else {
exit(json_encode(["status" => "ERROR", "msg" => lang("user does not exist", false), "user" => $VARS['manager']]));
}
} else {
if (user_exists_local($VARS['manager'])) {
if (user_exists_local($VARS['employee'])) {
$managerid = $database->select('accounts', 'uid', ['username' => strtolower($VARS['manager'])]);
$employeeid = $database->select('accounts', 'uid', ['username' => strtolower($VARS['employee'])]);
} else {
exit(json_encode(["status" => "ERROR", "msg" => lang("user does not exist", false), "user" => strtolower($VARS['employee'])]));
}
} else {
exit(json_encode(["status" => "ERROR", "msg" => lang("user does not exist", false), "user" => strtolower($VARS['manager'])]));
}
}
if ($database->has('managers', ['AND' => ['managerid' => $managerid, 'employeeid' => $employeeid]])) {
exit(json_encode(["status" => "OK", "managerof" => true]));
} else {
exit(json_encode(["status" => "OK", "managerof" => false]));
}
break;
case "getmanaged":
if ($VARS['uid']) {
if ($database->has("accounts", ['uid' => $VARS['uid']])) {
$managerid = $VARS['uid'];
} else {
exit(json_encode(["status" => "ERROR", "msg" => lang("user does not exist", false)]));
}
} else if ($VARS['username']) {
if ($database->has("accounts", ['username' => strtolower($VARS['username'])])) {
$managerid = $database->select('accounts', 'uid', ['username' => strtolower($VARS['username'])]);
} else {
exit(json_encode(["status" => "ERROR", "msg" => lang("user does not exist", false)]));
}
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
if ($VARS['get'] == "username") {
$managed = $database->select('managers', ['[>]accounts' => ['employeeid' => 'uid']], 'username', ['managerid' => $managerid]);
} else {
$managed = $database->select('managers', 'employeeid', ['managerid' => $managerid]);
}
exit(json_encode(["status" => "OK", "employees" => $managed]));
break;
case "getmanagers":
if ($VARS['uid']) {
if ($database->has("accounts", ['uid' => $VARS['uid']])) {
$empid = $VARS['uid'];
} else {
exit(json_encode(["status" => "ERROR", "msg" => lang("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" => lang("user does not exist", false)]));
}
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
$managers = $database->select('managers', 'managerid', ['employeeid' => $empid]);
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 (is_empty($VARS['code'])) {
http_response_code(400);
die("\"400 Bad Request\"");
}
$perm = $VARS['code'];
if ($VARS['uid']) {
if ($database->has("accounts", ['uid' => $VARS['uid']])) {
$user = $database->select('accounts', ['username'], ['uid' => $VARS['uid']])[0]['username'];
} else {
exit(json_encode(["status" => "ERROR", "msg" => lang("user does not exist", false)]));
}
} else if ($VARS['username']) {
if ($database->has("accounts", ['username' => strtolower($VARS['username'])])) {
$user = $VARS['username'];
} else {
exit(json_encode(["status" => "ERROR", "msg" => lang("user does not exist", false)]));
}
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
$hasperm = account_has_permission($user, $perm);
exit(json_encode(["status" => "OK", "has_permission" => $hasperm]));
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_exists($VARS['username'])) {
http_response_code(400);
die("\"400 Bad Request\"");
}
$appname = "???";
if (!is_empty($VARS['appname'])) {
$appname = $VARS['appname'];
}
$result = sendLoginAlertEmail($VARS['username'], $appname);
if ($result === TRUE) {
exit(json_encode(["status" => "OK"]));
}
exit(json_encode(["status" => "ERROR", "msg" => $result]));
default:
http_response_code(404);
die(json_encode("404 Not Found: the requested action is not available."));
}
/* } catch (Exception $e) {
header("HTTP/1.1 500 Internal Server Error");
die("\"500 Internal Server Error\"");
} */
// Load in new API from legacy location (a.k.a. here)
require __DIR__ . "/api/index.php";

5
api/.htaccess Normal file
View File

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

View File

@ -0,0 +1,9 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
sendJsonResp(null, "OK", ["account" => User::byUsername($VARS['username'])->getStatus()->getString()]);

View File

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

View File

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

View File

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

39
api/actions/auth.php Normal file
View File

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

View File

@ -0,0 +1,15 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
try {
$uid = LoginKey::getuid($VARS['code']);
exitWithJson(["status" => "OK", "uid" => $uid]);
} catch (Exception $ex) {
sendJsonResp("", "ERROR");
}

24
api/actions/checkpin.php Normal file
View File

@ -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'])]);

15
api/actions/codelogin.php Normal file
View File

@ -0,0 +1,15 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
$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");
}

View File

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

10
api/actions/getgroups.php Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@ -0,0 +1,9 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
sendJsonResp(null, "OK", ["otp" => User::byUsername($VARS['username'])->has2fa()]);

View File

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

16
api/actions/listapps.php Normal file
View File

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

46
api/actions/login.php Normal file
View File

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

View File

@ -0,0 +1,9 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
exitWithJson(["status" => "OK", "mobile" => $SETTINGS['mobile_enabled']]);

View File

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

View File

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

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

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

View File

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

View File

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

20
api/actions/userinfo.php Normal file
View File

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

View File

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

View File

@ -0,0 +1,15 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
$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]);
}

267
api/apisettings.php Normal file
View File

@ -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"
],
];

164
api/functions.php Normal file
View File

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

88
api/index.php Normal file
View File

@ -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"];

222
app.php Normal file
View File

@ -0,0 +1,222 @@
<?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.");
}
require_once __DIR__ . "/pages.php";
$pageid = "home";
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")) {
$pageid = $pg;
} else {
$pageid = "404";
}
}
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);
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.bundle.min.js>; rel=preload; as=script", false);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo $SETTINGS['site_title']; ?></title>
<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">
<?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'])) {
foreach (PAGES[$pageid]['styles'] as $style) {
echo "<link href=\"$style\" rel=\"stylesheet\">\n";
header("Link: <$style>; rel=preload; as=style", false);
}
}
?>
</head>
<body>
<?php
// Alert messages
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 {
// 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">
<div class="col-11 col-sm-6 col-md-5 col-lg-4 col-xl-4">
<div class="alert alert-dismissible alert-$alerttype mt-2 p-0 border-0 shadow">
<div class="p-2 pl-3">
<button type="button" class="close">&times;</button>
<i class="fas fa-$alerticon"></i> $alertmsg
</div>
<div class="progress">
<div class="progress-bar bg-$alerttype w-0" id="msg-alert-timeout-bar"></div>
</div>
</div>
</div>
</div>
END;
}
?>
<?php
// Adjust as needed
$navbar_breakpoint = "md";
// For mobile app
echo "<script nonce=\"$SECURE_NONCE\">var navbar_breakpoint = \"$navbar_breakpoint\";</script>"
?>
<nav class="navbar navbar-expand-<?php echo $navbar_breakpoint; ?> navbar-light bg-orange fixed-top">
<button class="navbar-toggler my-0 py-0" type="button" data-toggle="collapse" data-target="#navbar-collapse" aria-controls="navbar-collapse" aria-expanded="false" aria-label="Toggle navigation">
<!--<i class="fas fa-bars"></i>-->
<span class="navbar-toggler-icon"></span>
</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 $SETTINGS['site_title']; ?>
</a>
<div class="collapse navbar-collapse py-0" id="navbar-collapse">
<div class="navbar-nav mr-auto py-0">
<?php
$curpagefound = false;
foreach (PAGES as $id => $pg) {
if (isset($pg['navbar']) && $pg['navbar'] === TRUE) {
if ($pageid == $id) {
$curpagefound = true;
?>
<span class="nav-item py-<?php echo $navbar_breakpoint; ?>-0 active">
<?php
} else {
?>
<span class="nav-item py-<?php echo $navbar_breakpoint; ?>-0">
<?php
}
?>
<a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="app.php?page=<?php echo $id; ?>">
<?php
if (isset($pg['icon'])) {
?><i class="<?php echo $pg['icon']; ?> fa-fw"></i> <?php
}
$Strings->get($pg['title']);
?>
</a>
</span>
<?php
}
}
?>
</div>
<div class="navbar-nav ml-auto py-0" id="navbar-right">
<span class="nav-item py-<?php echo $navbar_breakpoint; ?>-0">
<a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="app.php">
<i class="fas fa-user fa-fw"></i><span>&nbsp;<?php echo $_SESSION['realname'] ?></span>
</a>
</span>
<span class="nav-item mr-auto py-<?php echo $navbar_breakpoint; ?>-0">
<a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="action.php?action=signout">
<i class="fas fa-sign-out-alt fa-fw"></i><span>&nbsp;<?php $Strings->get("sign out") ?></span>
</a>
</span>
</div>
</div>
</nav>
<div class="container" id="main-content">
<div>
<?php
include_once __DIR__ . '/pages/' . $pageid . ".php";
?>
</div>
<div class="footer">
<?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.bundle.min.js"></script>
<script src="static/js/app.js"></script>
<?php
// custom page scripts
if (isset(PAGES[$pageid]['scripts'])) {
foreach (PAGES[$pageid]['scripts'] as $script) {
echo "<script src=\"$script\"></script>\n";
header("Link: <$script>; rel=preload; as=script", false);
}
}
?>
</body>
</html>

View File

@ -1,13 +0,0 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
dieifnotloggedin();
$APPS["404_error"]["title"] = lang("404 error", false);
$APPS["404_error"]["icon"] = "times";
$APPS["404_error"]["type"] = "warning";
$APPS["404_error"]["content"] = "<h4>" . lang("page not found", false) . "</h4>";
?>

View File

@ -1,24 +0,0 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
dieifnotloggedin();
addMultiLangStrings(["en_us" => [
"manage account security" => "Manage account security",
"manage security description" => "Review security features or change your password."
]
]);
$APPS["account_security"]["i18n"] = TRUE;
$APPS["account_security"]["title"] = "account security";
$APPS["account_security"]["icon"] = "lock";
$APPS["account_security"]["type"] = "brown";
$content = "<p>"
. lang("manage security description", false)
. '</p> '
. '<a href="home.php?page=security" class="btn btn-primary btn-block">'
. lang("manage account security", false)
. '</a>';
$APPS["account_security"]["content"] = $content;
?>

View File

@ -1,21 +0,0 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
dieifnotloggedin();
$APPS["change_password"]["title"] = "Change Password";
$APPS["change_password"]["icon"] = "key";
$APPS["change_password"]["content"] = <<<CONTENTEND
<form action="action.php" method="POST">
<input type="password" class="form-control" name="oldpass" placeholder="Current password" />
<input type="password" class="form-control" name="newpass" placeholder="New password" />
<input type="password" class="form-control" name="conpass" placeholder="New password (again)" />
<input type="hidden" name="action" value="chpasswd" />
<input type="hidden" name="source" value="security" />
<br />
<button type="submit" class="btn btn-success btn-sm btn-block">Change Password</button>
</form>
CONTENTEND;

View File

@ -1,24 +0,0 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
dieifnotloggedin();
addMultiLangStrings(["en_us" => [
"inventory" => "Inventory",
"open inventory system" => "Open the inventory system"
]
]);
$APPS["inventory_link"]["i18n"] = TRUE;
$APPS["inventory_link"]["title"] = "inventory";
$APPS["inventory_link"]["icon"] = "cubes";
$APPS["inventory_link"]["type"] = "teal";
$content = "<p class='mobile-app-hide'>" . lang("open inventory system", false) . '</p><a href="' . INVENTORY_HOME . '" class="btn btn-primary btn-block mobile-app-hide">' . lang("open app", false) . ' &nbsp;<i class="fa fa-external-link-square"></i></a>';
$APPS["inventory_link"]["content"] = $content;
require_once __DIR__ . "/../lib/login.php";
if (account_has_permission($_SESSION['username'], "INV_VIEW") !== true) {
unset($APPS['inventory_link']);
}
?>

View File

@ -1,61 +0,0 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
dieifnotloggedin();
require_once __DIR__ . "/../lib/login.php";
addMultiLangStrings(["en_us" => [
"qwikclock" => "QwikClock",
"punch in" => "Punch in",
"punch out" => "Punch out",
"permission denied" => "You do not have permission to do that."
]
]);
$APPS["qwikclock_inout"]["i18n"] = TRUE;
$APPS["qwikclock_inout"]["title"] = "qwikclock";
$APPS["qwikclock_inout"]["icon"] = "clock-o";
$APPS["qwikclock_inout"]["type"] = "blue";
$content = "";
use GuzzleHttp\Exception\ClientException;
if (!is_empty($_GET['qwikclock']) && ($_GET['qwikclock'] === "punchin" || $_GET['qwikclock'] === "punchout")) {
try {
$client = new GuzzleHttp\Client();
$response = $client->request('POST', QWIKCLOCK_API, ['form_params' => [
'action' => $_GET['qwikclock'],
'username' => $_SESSION['username'],
'password' => $_SESSION['password']
]]);
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
$content = "<div class=\"alert alert-success alert-dismissable\"><button type=\"button\" class=\"close\">&times;</button>" . $resp['msg'] . "</div>";
} else {
$content = "<div class=\"alert alert-danger alert-dismissable\"><button type=\"button\" class=\"close\">&times;</button>" . $resp['msg'] . "</div>";
}
} catch (ClientException $e) {
if ($e->getResponse()->getStatusCode() == 403) {
$content = "<div class=\"alert alert-danger alert-dismissable\"><button type=\"button\" class=\"close\">&times;</button>" . lang("permission denied", false) . "</div>";
}
} catch (Exception $e) {
$content = "<div class=\"alert alert-danger alert-dismissable\"><button type=\"button\" class=\"close\">&times;</button>" . lang("error loading widget", false) . " " . $e->getMessage() . "</div>";
}
}
$lang_punchin = lang("punch in", false);
$lang_punchout = lang("punch out", false);
$content .= <<<END
<a href="home.php?&qwikclock=punchin" class="btn btn-block btn-success btn-lg"><i class="fa fa-play"></i> $lang_punchin</a>
<a href="home.php?qwikclock=punchout" class="btn btn-block btn-danger btn-lg"><i class="fa fa-stop"></i> $lang_punchout</a>
END;
$content .= '<br /><a href="' . QWIKCLOCK_HOME . '" class="btn btn-primary btn-block mobile-app-hide">' . lang("open app", false) . ' &nbsp;<i class="fa fa-external-link-square"></i></a>';
$APPS["qwikclock_inout"]["content"] = $content;
if (account_has_permission($_SESSION['username'], "QWIKCLOCK") !== true) {
unset($APPS['qwikclock_inout']);
}
?>

View File

@ -1,34 +0,0 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
dieifnotloggedin();
// Additional i18n strings
addMultiLangStrings(["en_us" => [
"sample app" => "Sample Application",
]
]);
// Set to true to automatically parse the app title as a language string.
$APPS["sample_app"]["i18n"] = TRUE;
// App title.
$APPS["sample_app"]["title"] = "sample app";
// App icon, from FontAwesome.
$APPS["sample_app"]["icon"] = "rocket";
// App content.
$APPS["sample_app"]["content"] = <<<'CONTENTEND'
<div class="list-group">
<div class="list-group-item">
Item 1
</div>
<div class="list-group-item">
Item 2
</div>
<div class="list-group-item">
Item 3
</div>
</div>
CONTENTEND;
?>

View File

@ -1,77 +0,0 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
dieifnotloggedin();
use OTPHP\Factory;
use Endroid\QrCode\QrCode;
// extra login utils
require_once __DIR__ . "/../lib/login.php";
$APPS["setup_2fa"]["title"] = lang("setup 2fa", false);
$APPS["setup_2fa"]["icon"] = "lock";
if (userHasTOTP($_SESSION['username'])) {
$APPS["setup_2fa"]["content"] = '<div class="alert alert-info"><i class="fa fa-info-circle"></i> ' . lang("2fa active", false) . '</div>'
. '<a href="action.php?action=rm2fa&source=security" class="btn btn-warning btn-sm btn-block">'
. lang("remove 2fa", false) . '</a>';
} else if ($_GET['2fa'] == "generate") {
$codeuri = newTOTP($_SESSION['username']);
$userdata = $database->select('accounts', ['email', 'authsecret', 'realname'], ['username' => $_SESSION['username']])[0];
$label = SYSTEM_NAME . ":" . is_null($userdata['email']) ? $userdata['realname'] : $userdata['email'];
$issuer = SYSTEM_NAME;
$qrCode = new QrCode($codeuri);
$qrCode->setSize(200);
$qrCode->setErrorCorrection("H");
$qrcode = $qrCode->getDataUri();
$totp = Factory::loadFromProvisioningUri($codeuri);
$codesecret = $totp->getSecret();
$chunk_secret = trim(chunk_split($codesecret, 4, ' '));
$lang_manualsetup = lang("manual setup", false);
$lang_secretkey = lang("secret key", false);
$lang_label = lang("label", false);
$lang_issuer = lang("issuer", false);
$lang_entercode = lang("enter otp code", false);
$APPS["setup_2fa"]["content"] = '<div class="alert alert-info"><i class="fa fa-info-circle"></i> ' . lang("scan 2fa qrcode", false) . '</div>' . <<<END
<style nonce="$SECURE_NONCE">
.margintop-15px {
margin-top: 15px;
}
.mono-chunk {
text-align: center;
font-size: 110%;
font-family: monospace;
}
</style>
<img src="$qrcode" class="img-responsive qrcode" />
<form action="action.php" method="POST" class="margintop-15px">
<input type="text" name="totpcode" class="form-control" placeholder="$lang_entercode" minlength=6 maxlength=6 required />
<br />
<input type="hidden" name="action" value="add2fa" />
<input type="hidden" name="source" value="security" />
<input type="hidden" name="secret" value="$codesecret" />
<button type="submit" class="btn btn-success btn-sm btn-block">
END
. lang("confirm 2fa", false) . <<<END
</button>
</form>
<div class="panel panel-default margintop-15px">
<div class="panel-body">
<b>$lang_manualsetup</b>
<br /><label>$lang_secretkey:</label>
<div class="well well-sm mono-chunk">$chunk_secret</div>
<br /><label>$lang_label:</label>
<div class="well well-sm mono-chunk">$label</div>
<br /><label>$lang_issuer:</label>
<div class="well well-sm mono-chunk">$issuer</div>
</div>
</div>
END;
} else {
$APPS["setup_2fa"]["content"] = '<div class="alert alert-info"><i class="fa fa-info-circle"></i> ' . lang("2fa explained", false) . '</div>'
. '<a class="btn btn-success btn-sm btn-block" href="home.php?page=security&2fa=generate">'
. lang("enable 2fa", false) . '</a>';
}

View File

@ -1,116 +0,0 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
dieifnotloggedin();
use Endroid\QrCode\QrCode;
if (MOBILE_ENABLED) {
addMultiLangStrings(["en_us" => [
"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.",
"generate sync" => "Create new sync code",
"active sync codes" => "Active codes",
"no active codes" => "No active codes.",
"done adding sync code" => "Done adding code",
"manual setup" => "Manual Setup:",
"sync key" => "Sync key:",
"url" => "URL:",
]
]);
$APPS["sync_mobile"]["title"] = lang("sync mobile", false);
$APPS["sync_mobile"]["icon"] = "mobile";
if (!is_empty($_GET['delsynccode'])) {
if ($database->has("mobile_codes", ["AND" => ["uid" => $_SESSION['uid'], "codeid" => $_GET['delsynccode']]])) {
$database->delete("mobile_codes", ["AND" => ["uid" => $_SESSION['uid'], "codeid" => $_GET['delsynccode']]]);
}
}
if ($_GET['mobilecode'] == "generate") {
if (!is_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 {
$code = strtoupper(substr(md5(mt_rand() . uniqid("", true)), 0, 20));
$database->insert('mobile_codes', ['uid' => $_SESSION['uid'], 'code' => $code]);
}
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->setSize(200);
$qrCode->setErrorCorrection("H");
$qrcode = $qrCode->getDataUri();
$chunk_code = trim(chunk_split($code, 5, ' '));
$lang_done = lang("done adding sync code", false);
$APPS["sync_mobile"]["content"] = '<div class="alert alert-info"><i class="fa fa-info-circle"></i> '
. lang("scan sync qrcode", false)
. '</div>'
. <<<END
<style nonce="$SECURE_NONCE">
.margintop-15px {
margin-top: 15px;
}
.mono-chunk {
text-align: center;
font-size: 110%;
font-family: monospace;
}
</style>
<img src="$qrcode" class="img-responsive qrcode" />
<div class="panel panel-default margintop-15px">
<div class="panel-body">
END
. "<b>" . lang("manual setup", false) . "</b><br /><label>" . lang("username", false) . ":</label>"
. '<div class="well well-sm mono-chunk">' . $_SESSION['username'] . '</div>'
. "<label>" . lang("sync key", false) . "</label>"
. <<<END
<div class="well well-sm mono-chunk">$chunk_code</div>
END
. "<label>" . lang("url", false) . "</label>"
. <<<END
<div class="well well-sm mono-chunk">$url</div>
</div>
</div>
<a class="btn btn-success btn-sm btn-block" href="home.php?page=security">$lang_done</a>
END;
} else {
$activecodes = $database->select("mobile_codes", ["codeid", "code"], ["uid" => $_SESSION['uid']]);
$content = '<div class="alert alert-info"><i class="fa fa-info-circle"></i> ' . lang("sync explained", false) . '</div>'
. '<a class="btn btn-success btn-sm btn-block" href="home.php?page=security&mobilecode=generate">'
. lang("generate sync", false) . '</a>';
$content .= "<br /><b>" . lang("active sync codes", false) . ":</b><br />";
$content .= "<div class='list-group'>";
if (count($activecodes) > 0) {
foreach ($activecodes as $c) {
$content .= "<div class='list-group-item mobilekey'><span id=\"mobilecode\">" . trim(chunk_split($c['code'], 5, ' ')) . "</span> <span class='tinybuttons'><a class='btn btn-primary btn-sm' href='home.php?page=security&mobilecode=generate&showsynccode=" . $c['codeid'] . "'><i class='fa fa-qrcode'></i></a> <a class='btn btn-danger btn-sm' href='home.php?page=security&delsynccode=" . $c['codeid'] . "'><i class='fa fa-trash'></i></a></span></div>";
}
} else {
$content .= "<div class='list-group-item'>" . lang("no active codes", false) . "</div>";
}
$content .= "</div>";
$content .= <<<END
<style nonce="$SECURE_NONCE">
.mobilekey {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.mobilekey #mobilecode {
font-family: Ubuntu Mono,monospace;
flex-shrink: 0;
}
</style>
END;
$APPS["sync_mobile"]["content"] = $content;
}
}

View File

@ -1,67 +0,0 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
dieifnotloggedin();
addMultiLangStrings(["en_us" => [
"messages" => "Messages",
"no messages" => "No messages found."
]
]);
$APPS["taskfloor_messages"]["i18n"] = TRUE;
$APPS["taskfloor_messages"]["title"] = "messages";
$APPS["taskfloor_messages"]["icon"] = "comments";
$APPS["taskfloor_messages"]["type"] = "deep-purple";
$content = "";
use GuzzleHttp\Exception\ClientException;
try {
$client = new GuzzleHttp\Client();
$response = $client->request('POST', TASKFLOOR_API, ['form_params' => [
'action' => "getmsgs",
'username' => $_SESSION['username'],
'password' => $_SESSION['password'],
'max' => 5
]]);
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
if (count($resp['messages']) > 0) {
$content = '<div class="list-group">';
foreach ($resp['messages'] as $msg) {
$content .= '<div class="list-group-item">';
$content .= $msg['text'];
$fromuser = $msg['from']['username'];
$fromname = $msg['from']['name'];
$touser = $msg['to']['username'];
$toname = $msg['to']['name'];
$content .= <<<END
<br />
<span class="small">
<span data-toggle="tooltip" title="$fromuser">$fromname</span>
<i class="fa fa-caret-right"></i>
<span data-toggle="tooltip" title="$touser">$toname</span>
</span>
END;
$content .= '</div>';
}
$content .= "</div>";
} else {
$content = "<div class=\"alert alert-info\">" . lang("no messages", false) . "</div>";
}
}
$content .= '<a href="' . TASKFLOOR_HOME . '" class="btn btn-primary btn-block mobile-app-hide">' . lang("open app", false) . ' &nbsp;<i class="fa fa-external-link-square"></i></a>';
$APPS["taskfloor_messages"]["content"] = $content;
} catch (ClientException $e) {
if ($e->getResponse()->getStatusCode() == 403) {
unset($APPS['taskfloor_messages']);
}
} catch (Exception $e) {
$content = "<div class=\"alert alert-danger\">" . lang("error loading widget", false) . " " . $e->getMessage() . "</div>";
$content .= '<a href="' . TASKFLOOR_HOME . '" class="btn btn-primary btn-block mobile-app-hide">' . lang("open app", false) . ' &nbsp;<i class="fa fa-external-link-square"></i></a>';
$APPS["taskfloor_messages"]["content"] = $content;
}
?>

View File

@ -1,56 +0,0 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
dieifnotloggedin();
addMultiLangStrings(["en_us" => [
"tasks" => "Tasks",
"no tasks found" => "No tasks found."
]
]);
$APPS["taskfloor_tasks"]["i18n"] = TRUE;
$APPS["taskfloor_tasks"]["title"] = "tasks";
$APPS["taskfloor_tasks"]["icon"] = "tasks";
$APPS["taskfloor_tasks"]["type"] = "blue-grey";
$content = "";
use GuzzleHttp\Exception\ClientException;
try {
$client = new GuzzleHttp\Client();
$response = $client->request('POST', TASKFLOOR_API, ['form_params' => [
'action' => "gettasks",
'username' => $_SESSION['username'],
'password' => $_SESSION['password'],
'max' => 5
]]);
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
if (count($resp['tasks']) > 0) {
$content = '<div class="list-group">';
foreach ($resp['tasks'] as $task) {
$content .= '<div class="list-group-item">';
$content .= '<i class="fa fa-fw fa-' . $task['icon'] . '"></i> ' . $task['title'] . '';
$content .= '</div>';
}
$content .= "</div>";
} else {
$content = "<div class=\"alert alert-success\">" . lang("no tasks found", false) . "</div>";
}
}
$content .= '<a href="' . TASKFLOOR_HOME . '" class="btn btn-primary btn-block mobile-app-hide">' . lang("open app", false) . ' &nbsp;<i class="fa fa-external-link-square"></i></a>';
$APPS["taskfloor_tasks"]["content"] = $content;
} catch (ClientException $e) {
if ($e->getResponse()->getStatusCode() == 403) {
unset($APPS['taskfloor_tasks']);
}
} catch (Exception $e) {
$content = "<div class=\"alert alert-danger\">" . lang("error loading widget", false) . " " . $e->getMessage() . "</div>";
$content .= '<a href="' . TASKFLOOR_HOME . '" class="btn btn-primary btn-block mobile-app-hide">' . lang("open app", false) . ' &nbsp;<i class="fa fa-external-link-square"></i></a>';
$APPS["taskfloor_tasks"]["content"] = $content;
}
?>

View File

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

897
composer.lock generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -1,5 +1,5 @@
-- MySQL Script generated by MySQL Workbench
-- Mon 20 Nov 2017 08:36:18 PM MST
-- Mon 11 Feb 2019 04:07:57 PM MST
-- Model: New Model Version: 1.0
-- MySQL Workbench Forward Engineering
@ -12,15 +12,9 @@ SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES';
-- -----------------------------------------------------
-- -----------------------------------------------------
-- Schema accounthub
-- Table `acctstatus`
-- -----------------------------------------------------
CREATE SCHEMA IF NOT EXISTS `accounthub` DEFAULT CHARACTER SET utf8 ;
USE `accounthub` ;
-- -----------------------------------------------------
-- Table `accounthub`.`acctstatus`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `accounthub`.`acctstatus` (
CREATE TABLE IF NOT EXISTS `acctstatus` (
`statusid` INT NOT NULL AUTO_INCREMENT,
`statuscode` VARCHAR(45) NOT NULL,
PRIMARY KEY (`statusid`),
@ -30,9 +24,9 @@ ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `accounthub`.`accttypes`
-- Table `accttypes`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `accounthub`.`accttypes` (
CREATE TABLE IF NOT EXISTS `accttypes` (
`typeid` INT NOT NULL AUTO_INCREMENT,
`typecode` VARCHAR(45) NOT NULL,
PRIMARY KEY (`typeid`),
@ -42,15 +36,16 @@ ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `accounthub`.`accounts`
-- Table `accounts`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `accounthub`.`accounts` (
CREATE TABLE IF NOT EXISTS `accounts` (
`uid` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(190) NOT NULL,
`password` VARCHAR(255) NULL,
`realname` VARCHAR(255) NOT NULL,
`email` VARCHAR(255) NULL DEFAULT 'NOEMAIL@EXAMPLE.COM',
`authsecret` VARCHAR(100) NULL,
`pin` VARCHAR(10) NULL,
`phone1` VARCHAR(45) NOT NULL,
`phone2` VARCHAR(45) NOT NULL,
`acctstatus` INT NOT NULL DEFAULT 0,
@ -64,66 +59,33 @@ CREATE TABLE IF NOT EXISTS `accounthub`.`accounts` (
INDEX `fk_accounts_accttypes1_idx` (`accttype` ASC),
CONSTRAINT `fk_accounts_acctstatus`
FOREIGN KEY (`acctstatus`)
REFERENCES `accounthub`.`acctstatus` (`statusid`)
REFERENCES `acctstatus` (`statusid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_accounts_accttypes1`
FOREIGN KEY (`accttype`)
REFERENCES `accounthub`.`accttypes` (`typeid`)
REFERENCES `accttypes` (`typeid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `accounthub`.`apps`
-- Table `apikeys`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `accounthub`.`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 `accounthub`.`available_apps`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `accounthub`.`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 `accounthub`.`apps` (`appid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_apps_has_accounts_accounts1`
FOREIGN KEY (`uid`)
REFERENCES `accounthub`.`accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `accounthub`.`apikeys`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `accounthub`.`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;
-- -----------------------------------------------------
-- Table `accounthub`.`groups`
-- Table `groups`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `accounthub`.`groups` (
`groupid` INT NOT NULL,
CREATE TABLE IF NOT EXISTS `groups` (
`groupid` INT NOT NULL AUTO_INCREMENT,
`groupname` VARCHAR(45) NOT NULL,
PRIMARY KEY (`groupid`),
UNIQUE INDEX `groupid_UNIQUE` (`groupid` ASC),
@ -132,9 +94,9 @@ ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `accounthub`.`assigned_groups`
-- Table `assigned_groups`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `accounthub`.`assigned_groups` (
CREATE TABLE IF NOT EXISTS `assigned_groups` (
`groupid` INT NOT NULL,
`uid` INT NOT NULL,
PRIMARY KEY (`groupid`, `uid`),
@ -142,42 +104,42 @@ CREATE TABLE IF NOT EXISTS `accounthub`.`assigned_groups` (
INDEX `fk_groups_has_accounts_groups1_idx` (`groupid` ASC),
CONSTRAINT `fk_groups_has_accounts_groups1`
FOREIGN KEY (`groupid`)
REFERENCES `accounthub`.`groups` (`groupid`)
REFERENCES `groups` (`groupid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_groups_has_accounts_accounts1`
FOREIGN KEY (`uid`)
REFERENCES `accounthub`.`accounts` (`uid`)
REFERENCES `accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `accounthub`.`managers`
-- Table `managers`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `accounthub`.`managers` (
CREATE TABLE IF NOT EXISTS `managers` (
`managerid` INT NOT NULL,
`employeeid` INT NOT NULL,
PRIMARY KEY (`managerid`, `employeeid`),
INDEX `fk_managers_accounts2_idx` (`employeeid` ASC),
CONSTRAINT `fk_managers_accounts1`
FOREIGN KEY (`managerid`)
REFERENCES `accounthub`.`accounts` (`uid`)
REFERENCES `accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_managers_accounts2`
FOREIGN KEY (`employeeid`)
REFERENCES `accounthub`.`accounts` (`uid`)
REFERENCES `accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `accounthub`.`logtypes`
-- Table `logtypes`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `accounthub`.`logtypes` (
CREATE TABLE IF NOT EXISTS `logtypes` (
`logtype` INT NOT NULL,
`typename` VARCHAR(45) NULL,
PRIMARY KEY (`logtype`),
@ -186,9 +148,9 @@ ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `accounthub`.`authlog`
-- Table `authlog`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `accounthub`.`authlog` (
CREATE TABLE IF NOT EXISTS `authlog` (
`logid` INT NOT NULL AUTO_INCREMENT,
`logtime` DATETIME NOT NULL,
`logtype` INT NOT NULL,
@ -201,21 +163,21 @@ CREATE TABLE IF NOT EXISTS `accounthub`.`authlog` (
INDEX `fk_authlog_accounts1_idx` (`uid` ASC),
CONSTRAINT `fk_authlog_logtypes1`
FOREIGN KEY (`logtype`)
REFERENCES `accounthub`.`logtypes` (`logtype`)
REFERENCES `logtypes` (`logtype`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_authlog_accounts1`
FOREIGN KEY (`uid`)
REFERENCES `accounthub`.`accounts` (`uid`)
REFERENCES `accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `accounthub`.`permissions`
-- Table `permissions`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `accounthub`.`permissions` (
CREATE TABLE IF NOT EXISTS `permissions` (
`permid` INT NOT NULL AUTO_INCREMENT,
`permcode` VARCHAR(45) NOT NULL,
`perminfo` VARCHAR(200) NULL,
@ -225,9 +187,9 @@ ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `accounthub`.`assigned_permissions`
-- Table `assigned_permissions`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `accounthub`.`assigned_permissions` (
CREATE TABLE IF NOT EXISTS `assigned_permissions` (
`uid` INT NOT NULL,
`permid` INT NOT NULL,
PRIMARY KEY (`uid`, `permid`),
@ -235,119 +197,246 @@ CREATE TABLE IF NOT EXISTS `accounthub`.`assigned_permissions` (
INDEX `fk_permissions_has_accounts_permissions1_idx` (`permid` ASC),
CONSTRAINT `fk_permissions_has_accounts_permissions1`
FOREIGN KEY (`permid`)
REFERENCES `accounthub`.`permissions` (`permid`)
REFERENCES `permissions` (`permid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_permissions_has_accounts_accounts1`
FOREIGN KEY (`uid`)
REFERENCES `accounthub`.`accounts` (`uid`)
REFERENCES `accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `accounthub`.`mobile_codes`
-- Table `mobile_codes`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `accounthub`.`mobile_codes` (
CREATE TABLE IF NOT EXISTS `mobile_codes` (
`codeid` INT NOT NULL AUTO_INCREMENT,
`uid` INT NOT NULL,
`code` VARCHAR(45) NULL,
`code` VARCHAR(45) NOT NULL DEFAULT '',
`description` VARCHAR(255) NOT NULL DEFAULT '',
PRIMARY KEY (`codeid`),
UNIQUE INDEX `codeid_UNIQUE` (`codeid` ASC),
INDEX `fk_mobile_codes_accounts1_idx` (`uid` ASC),
UNIQUE INDEX `code_UNIQUE` (`code` ASC),
CONSTRAINT `fk_mobile_codes_accounts1`
FOREIGN KEY (`uid`)
REFERENCES `accounthub`.`accounts` (`uid`)
REFERENCES `accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `accounthub`.`rate_limit`
-- Table `rate_limit`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `accounthub`.`rate_limit` (
CREATE TABLE IF NOT EXISTS `rate_limit` (
`ipaddr` VARCHAR(45) NOT NULL,
`lastaction` DATETIME NULL,
PRIMARY KEY (`ipaddr`))
ENGINE = MEMORY;
-- -----------------------------------------------------
-- Table `onetimekeys`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `onetimekeys` (
`key` VARCHAR(10) NOT NULL,
`uid` INT NOT NULL,
`expires` DATETIME NOT NULL,
INDEX `fk_onetimekeys_accounts1_idx` (`uid` ASC),
PRIMARY KEY (`key`),
UNIQUE INDEX `key_UNIQUE` (`key` ASC),
CONSTRAINT `fk_onetimekeys_accounts1`
FOREIGN KEY (`uid`)
REFERENCES `accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `notifications`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `notifications` (
`notificationid` INT NOT NULL AUTO_INCREMENT,
`uid` INT NOT NULL,
`timestamp` DATETIME NOT NULL,
`title` VARCHAR(255) NOT NULL,
`content` TINYTEXT NOT NULL,
`url` VARCHAR(255) NOT NULL,
`seen` TINYINT(1) NOT NULL DEFAULT 0,
`sensitive` TINYINT(1) NOT NULL,
`appid` VARCHAR(255) NULL,
PRIMARY KEY (`notificationid`, `uid`),
UNIQUE INDEX `notificationid_UNIQUE` (`notificationid` ASC),
INDEX `fk_notifications_accounts1_idx` (`uid` ASC),
CONSTRAINT `fk_notifications_accounts1`
FOREIGN KEY (`uid`)
REFERENCES `accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
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;
-- -----------------------------------------------------
-- Data for table `accounthub`.`acctstatus`
-- Data for table `acctstatus`
-- -----------------------------------------------------
START TRANSACTION;
USE `accounthub`;
INSERT INTO `accounthub`.`acctstatus` (`statusid`, `statuscode`) VALUES (1, 'NORMAL');
INSERT INTO `accounthub`.`acctstatus` (`statusid`, `statuscode`) VALUES (2, 'LOCKED_OR_DISABLED');
INSERT INTO `accounthub`.`acctstatus` (`statusid`, `statuscode`) VALUES (3, 'CHANGE_PASSWORD');
INSERT INTO `accounthub`.`acctstatus` (`statusid`, `statuscode`) VALUES (4, 'TERMINATED');
INSERT INTO `accounthub`.`acctstatus` (`statusid`, `statuscode`) VALUES (5, 'ALERT_ON_ACCESS');
INSERT INTO `acctstatus` (`statusid`, `statuscode`) VALUES (1, 'NORMAL');
INSERT INTO `acctstatus` (`statusid`, `statuscode`) VALUES (2, 'LOCKED_OR_DISABLED');
INSERT INTO `acctstatus` (`statusid`, `statuscode`) VALUES (3, 'CHANGE_PASSWORD');
INSERT INTO `acctstatus` (`statusid`, `statuscode`) VALUES (4, 'TERMINATED');
INSERT INTO `acctstatus` (`statusid`, `statuscode`) VALUES (5, 'ALERT_ON_ACCESS');
COMMIT;
-- -----------------------------------------------------
-- Data for table `accounthub`.`accttypes`
-- Data for table `accttypes`
-- -----------------------------------------------------
START TRANSACTION;
USE `accounthub`;
INSERT INTO `accounthub`.`accttypes` (`typeid`, `typecode`) VALUES (1, 'LOCAL');
INSERT INTO `accounthub`.`accttypes` (`typeid`, `typecode`) VALUES (2, 'LDAP');
INSERT INTO `accounthub`.`accttypes` (`typeid`, `typecode`) VALUES (3, 'LIGHT');
INSERT INTO `accttypes` (`typeid`, `typecode`) VALUES (1, 'LOCAL');
INSERT INTO `accttypes` (`typeid`, `typecode`) VALUES (2, 'EXTERNAL');
COMMIT;
-- -----------------------------------------------------
-- Data for table `accounthub`.`logtypes`
-- Data for table `logtypes`
-- -----------------------------------------------------
START TRANSACTION;
USE `accounthub`;
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (1, 'PORTAL_LOGIN_OK');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (2, 'PORTAL_LOGIN_FAILED');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (3, 'PASSWORD_CHANGED');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (4, 'API_LOGIN_OK');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (5, 'API_LOGIN_FAILED');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (6, 'PORTAL_BAD_AUTHCODE');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (7, 'API_BAD_AUTHCODE');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (8, 'BAD_CAPTCHA');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (9, '2FA_ADDED');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (10, '2FA_REMOVED');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (11, 'PORTAL_LOGOUT');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (12, 'API_AUTH_OK');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (13, 'API_AUTH_FAILED');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (14, 'API_BAD_KEY');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (15, 'LOG_CLEARED');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (16, 'USER_REMOVED');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (17, 'USER_ADDED');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (18, 'USER_EDITED');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (19, 'MOBILE_LOGIN_OK');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (20, 'MOBILE_LOGIN_FAILED');
INSERT INTO `accounthub`.`logtypes` (`logtype`, `typename`) VALUES (21, 'MOBILE_BAD_KEY');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (1, 'PORTAL_LOGIN_OK');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (2, 'PORTAL_LOGIN_FAILED');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (3, 'PASSWORD_CHANGED');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (4, 'API_LOGIN_OK');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (5, 'API_LOGIN_FAILED');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (6, 'PORTAL_BAD_AUTHCODE');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (7, 'API_BAD_AUTHCODE');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (8, 'BAD_CAPTCHA');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (9, '2FA_ADDED');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (10, '2FA_REMOVED');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (11, 'PORTAL_LOGOUT');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (12, 'API_AUTH_OK');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (13, 'API_AUTH_FAILED');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (14, 'API_BAD_KEY');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (15, 'LOG_CLEARED');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (16, 'USER_REMOVED');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (17, 'USER_ADDED');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (18, 'USER_EDITED');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (19, 'MOBILE_LOGIN_OK');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (20, 'MOBILE_LOGIN_FAILED');
INSERT INTO `logtypes` (`logtype`, `typename`) VALUES (21, 'MOBILE_BAD_KEY');
COMMIT;
-- -----------------------------------------------------
-- Data for table `accounthub`.`permissions`
-- Data for table `permissions`
-- -----------------------------------------------------
START TRANSACTION;
USE `accounthub`;
INSERT INTO `accounthub`.`permissions` (`permid`, `permcode`, `perminfo`) VALUES (1, 'ADMIN', 'System administrator');
INSERT INTO `accounthub`.`permissions` (`permid`, `permcode`, `perminfo`) VALUES (100, 'INV_VIEW', 'Access inventory system');
INSERT INTO `accounthub`.`permissions` (`permid`, `permcode`, `perminfo`) VALUES (101, 'INV_EDIT', 'Edit inventory system');
INSERT INTO `accounthub`.`permissions` (`permid`, `permcode`, `perminfo`) VALUES (200, 'TASKFLOOR', 'Access TaskFloor');
INSERT INTO `accounthub`.`permissions` (`permid`, `permcode`, `perminfo`) VALUES (300, 'QWIKCLOCK', 'Access QwikClock and punch in/out');
INSERT INTO `accounthub`.`permissions` (`permid`, `permcode`, `perminfo`) VALUES (301, 'QWIKCLOCK_MANAGE', 'Edit punches and other data for managed users');
INSERT INTO `accounthub`.`permissions` (`permid`, `permcode`, `perminfo`) VALUES (302, 'QWIKCLOCK_EDITSELF', 'Edit own punches and other data');
INSERT INTO `accounthub`.`permissions` (`permid`, `permcode`, `perminfo`) VALUES (303, 'QWIKCLOCK_ADMIN', 'Add and edit shifts and other data for all users');
INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (1, 'ADMIN', 'System administrator');
INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (100, 'INV_VIEW', 'Access inventory system');
INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (101, 'INV_EDIT', 'Edit inventory system');
INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (200, 'TASKFLOOR', 'Access TaskFloor');
INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (300, 'QWIKCLOCK', 'Access QwikClock and punch in/out');
INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (301, 'QWIKCLOCK_MANAGE', 'Edit punches and other data for managed users');
INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (302, 'QWIKCLOCK_EDITSELF', 'Edit own punches and other data');
INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (303, 'QWIKCLOCK_ADMIN', 'Add and edit shifts and other data for all users');
INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (400, 'SITEWRITER', 'Manage and edit websites, messages, and analytics');
INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (401, 'SITEWRITER_CONTACT', 'Manage messages sent via website contact forms');
INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (402, 'SITEWRITER_ANALYTICS', 'View website analytics');
INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (403, 'SITEWRITER_EDIT', 'Edit website content');
INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (404, 'SITEWRITER_FILES', 'Manage and upload files');
COMMIT;
-- -----------------------------------------------------
-- Data for table `userkeytypes`
-- -----------------------------------------------------
START TRANSACTION;
INSERT INTO `userkeytypes` (`typeid`, `typename`) VALUES (1, 'RSSAtomFeed');
INSERT INTO `userkeytypes` (`typeid`, `typename`) VALUES (2, 'Other');
COMMIT;

View File

@ -0,0 +1,54 @@
CREATE TABLE IF NOT EXISTS `onetimekeys` (
`key` VARCHAR(10) NOT NULL,
`uid` INT(11) NOT NULL,
`expires` DATETIME NOT NULL,
INDEX `fk_onetimekeys_accounts1_idx` (`uid` ASC),
PRIMARY KEY (`key`),
UNIQUE INDEX `key_UNIQUE` (`key` ASC),
CONSTRAINT `fk_onetimekeys_accounts1`
FOREIGN KEY (`uid`)
REFERENCES `accounthub`.`accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
CREATE TABLE IF NOT EXISTS `notifications` (
`notificationid` INT NOT NULL AUTO_INCREMENT,
`uid` INT NOT NULL,
`timestamp` DATETIME NOT NULL,
`title` VARCHAR(255) NOT NULL,
`content` TINYTEXT NOT NULL,
`url` VARCHAR(255) NOT NULL,
`seen` TINYINT(1) NOT NULL DEFAULT 0,
`sensitive` TINYINT(1) NOT NULL,
`appid` VARCHAR(255) NULL,
PRIMARY KEY (`notificationid`, `uid`),
UNIQUE INDEX `notificationid_UNIQUE` (`notificationid` ASC),
INDEX `fk_notifications_accounts1_idx` (`uid` ASC),
CONSTRAINT `fk_notifications_accounts1`
FOREIGN KEY (`uid`)
REFERENCES `accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
SET FOREIGN_KEY_CHECKS = 0;
ALTER TABLE `groups`
CHANGE COLUMN `groupid` `groupid` INT(11) NOT NULL AUTO_INCREMENT;
SET FOREIGN_KEY_CHECKS = 1;
ALTER TABLE `accounts`
ADD COLUMN `pin` VARCHAR(10) NULL DEFAULT NULL AFTER `authsecret`;
ALTER TABLE `mobile_codes`
CHANGE COLUMN `code` `code` VARCHAR(45) NOT NULL DEFAULT '',
ADD COLUMN `description` VARCHAR(255) NOT NULL DEFAULT '' AFTER `code`;
INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (400, 'SITEWRITER', 'Manage and edit websites, messages, and analytics');
INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (401, 'SITEWRITER_CONTACT', 'Manage messages sent via website contact forms');
INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (402, 'SITEWRITER_ANALYTICS', 'View website analytics');
INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (403, 'SITEWRITER_EDIT', 'Edit website content');
INSERT INTO `permissions` (`permid`, `permcode`, `perminfo`) VALUES (404, 'SITEWRITER_FILES', 'Manage and upload files');

View File

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

View File

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

89
feed.php Normal file
View File

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

249
home.php
View File

@ -1,253 +1,6 @@
<?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 ($_SESSION['loggedin'] != true) {
header('Location: index.php');
die("Session expired. Log in again to continue.");
} else if (is_empty($_SESSION['password'])) {
header('Location: index.php');
die("You need to log in again.");
}
require_once __DIR__ . "/pages.php";
$pageid = "home";
if (!is_empty($_GET['page'])) {
if (array_key_exists($_GET['page'], PAGES)) {
$pageid = $_GET['page'];
} else {
$pageid = "404";
}
}
?>
<!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 href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/font-awesome.min.css" rel="stylesheet">
<link href="static/css/material-color/material-color.min.css" rel="stylesheet">
<link href="static/css/app.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-4 col-sm-offset-3 col-md-offset-4 col-lg-offset-4">
<?php
if ((SHOW_ICON == "both" || SHOW_ICON == "app") && ICON_POSITION != "menu") {
if (MENU_BAR_STYLE != "fixed") {
?>
<img class="img-responsive banner-image" src="static/img/logo.svg" />
<?php
}
}
?>
</div>
</div>
<nav class="navbar navbar-default navbar-orange navbar-<?php echo MENU_BAR_STYLE; ?>-top">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<?php
if (SHOW_ICON == "both" || SHOW_ICON == "app") {
if (MENU_BAR_STYLE == "fixed" || ICON_POSITION == "menu") {
$src = "static/img/logo.svg";
if ($pageid != "home") {
$src = "static/img/up-arrow-black.png";
}
?>
<a class="navbar-brand" href="home.php">
<img src="<?php echo $src; ?>" />
</a>
<?php
}
}
?>
<a class="navbar-brand" href="home.php">
<?php
echo SITE_TITLE;
?>
</a>
</div>
<div class="collapse navbar-collapse" id="navbar-collapse">
<ul class="nav navbar-nav">
<?php
foreach (PAGES as $id => $pg) {
if ($pg['navbar'] === TRUE) {
if ($pageid == $id) {
?>
<li class="active">
<?php
} else {
?>
<li>
<?php } ?>
<a href="home.php?page=<?php echo $id; ?>">
<?php
if (isset($pg['icon'])) {
?>
<i class="fa fa-<?php echo $pg['icon']; ?> fa-fw"></i>
<?php } ?>
<?php lang($pg['title']) ?>
</a>
</li>
<?php
}
}
?>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="home.php"><i class="fa fa-user fa-fw"></i> <?php echo $_SESSION['realname'] ?></a></li>
<li><a href="action.php?action=signout"><i class="fa fa-sign-out fa-fw"></i> <?php lang("sign out") ?></a></li>
</ul>
</div>
</nav>
<?php
if (MENU_BAR_STYLE == "fixed") {
?>
<div class="pad-75px"></div>
<?php
}
?>
<div class="app-dock-container mobile-app-hide">
<div class="app-dock">
<?php
foreach (EXTERNAL_APPS as $a) {
?>
<div class="app-dock-item">
<p>
<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><?php echo $a['title']; ?></span>
</a>
</p>
</div>
<?php
}
?>
</div>
</div>
<?php
// Alert messages
if (!is_empty($_GET['msg']) && array_key_exists($_GET['msg'], MESSAGES)) {
// optional string generation argument
if (is_empty($_GET['arg'])) {
$alertmsg = lang(MESSAGES[$_GET['msg']]['string'], false);
} else {
$alertmsg = lang2(MESSAGES[$_GET['msg']]['string'], ["arg" => $_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;
}
echo <<<END
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-4 col-sm-offset-3 col-md-offset-4 col-lg-offset-4">
<div class="alert alert-dismissible alert-$alerttype">
<button type="button" class="close">&times;</button>
<i class="fa fa-$alerticon"></i> $alertmsg
</div>
</div>
</div>
END;
}
?>
<div class="row widget-box">
<?php
// Center the widgets horizontally on the screen
$appcount = 0;
foreach (APPS[$pageid] as $app) {
if (file_exists(__DIR__ . "/apps/" . $app . ".php")) {
include_once __DIR__ . "/apps/" . $app . ".php";
if (isset($APPS[$app])) {
$appcount++;
}
}
}
if ($appcount == 1) {
?>
<div class="hidden-xs col-sm-3 col-md-4 col-lg-4">
<!-- Empty placeholder column for nice center-align -->
</div>
<?php
} else if ($appcount == 2) {
?>
<div class="hidden-xs hidden-sm col-md-2 col-lg-2">
<!-- Empty placeholder column for nice center-align -->
</div>
<?php
}
// Load app widgets
foreach (APPS[$pageid] as $app) {
if (file_exists(__DIR__ . "/apps/" . $app . ".php")) {
include_once __DIR__ . "/apps/" . $app . ".php";
if (!isset($APPS[$app])) {
continue;
}
$apptitle = ($APPS[$app]['i18n'] === TRUE ? lang($APPS[$app]['title'], false) : $APPS[$app]['title']);
$appicon = (is_empty($APPS[$app]['icon']) ? "" : "fa fa-fw fa-" . $APPS[$app]['icon']);
$apptype = (is_empty($APPS[$app]['type']) ? "default" : $APPS[$app]['type']);
$appcontent = $APPS[$app]['content'];
echo <<<END
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-4">
<div class="panel panel-$apptype apppanel">
<div class="panel-heading">
<h3 class="panel-title"><i class="$appicon"></i> $apptitle </h3>
</div>
<div class="panel-body">
$appcontent
</div>
</div>
</div>
END;
}
}
?>
</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.2.1.min.js"></script>
<script src="static/js/bootstrap.min.js"></script>
<script src="static/js/app.js"></script>
</body>
</html>
header("Location: app.php");

328
index.php
View File

@ -1,245 +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";
require_once __DIR__ . "/lib/login.php";
// If we're logged in, we don't need to be here.
if ($_SESSION['loggedin'] && !is_empty($_SESSION['password'])) {
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.
} else if ($_SESSION['loggedin'] && is_empty($_SESSION['password'])) {
$alert = lang("sign in again", false);
$alerttype = "info";
}
/* Authenticate user */
$username_ok = false;
$multiauth = false;
$change_password = false;
if ($VARS['progress'] == "1") {
engageRateLimit();
if (!RECAPTCHA_ENABLED || (RECAPTCHA_ENABLED && verifyReCaptcha($VARS['g-recaptcha-response']))) {
$autherror = "";
if (user_exists($VARS['username'])) {
$status = get_account_status($VARS['username'], $error);
switch ($status) {
case "LOCKED_OR_DISABLED":
$alert = lang("account locked", false);
break;
case "TERMINATED":
$alert = lang("account terminated", false);
break;
case "CHANGE_PASSWORD":
$alert = lang("password expired", false);
$alerttype = "info";
$_SESSION['username'] = strtolower($VARS['username']);
$_SESSION['uid'] = $database->get('accounts', 'uid', ['username' => strtolower($VARS['username'])]);
$change_password = true;
break;
case "NORMAL":
$username_ok = true;
break;
case "ALERT_ON_ACCESS":
$mail_resp = sendLoginAlertEmail($VARS['username']);
if (DEBUG) {
var_dump($mail_resp);
}
$username_ok = true;
break;
default:
if (!is_empty($error)) {
$alert = $error;
} else {
$alert = lang("login error", false);
}
break;
}
if ($username_ok) {
if (authenticate_user($VARS['username'], $VARS['password'], $autherror)) {
$_SESSION['passok'] = true; // stop logins using only username and authcode
if (userHasTOTP($VARS['username'])) {
$multiauth = true;
$_SESSION['password'] = $VARS['password'];
} else {
doLoginUser($VARS['username'], $VARS['password']);
insertAuthLog(1, $_SESSION['uid']);
header('Location: home.php');
die("Logged in, go to home.php");
}
} else {
if (!is_empty($autherror)) {
$alert = $autherror;
insertAuthLog(2, null, "Username: " . $VARS['username']);
} else {
$alert = lang("login incorrect", false);
insertAuthLog(2, null, "Username: " . $VARS['username']);
}
}
}
} else { // User does not exist anywhere
$alert = lang("login incorrect", false);
insertAuthLog(2, null, "Username: " . $VARS['username']);
}
} else {
$alert = lang("captcha error", false);
insertAuthLog(8, null, "Username: " . $VARS['username']);
}
} else if ($VARS['progress'] == "2") {
engageRateLimit();
if ($_SESSION['passok'] !== true) {
// stop logins using only username and authcode
sendError("Password integrity check failed!");
}
if (verifyTOTP($VARS['username'], $VARS['authcode'])) {
doLoginUser($VARS['username'], $VARS['password']);
insertAuthLog(1, $_SESSION['uid']);
header('Location: home.php');
die("Logged in, go to home.php");
} else {
$alert = lang("2fa incorrect", false);
insertAuthLog(6, null, "Username: " . $VARS['username']);
}
} else if ($VARS['progress'] == "chpasswd") {
engageRateLimit();
if (!is_empty($_SESSION['username'])) {
$error = [];
$result = change_password($VARS['oldpass'], $VARS['newpass'], $VARS['conpass'], $error);
if ($result === TRUE) {
$alert = lang(MESSAGES["password_updated"]["string"], false);
$alerttype = MESSAGES["password_updated"]["type"];
}
switch (count($error)) {
case 0:
break;
case 1:
$alert = lang(MESSAGES[$error[0]]["string"], false);
$alerttype = MESSAGES[$error[0]]["type"];
break;
case 2:
$alert = lang2(MESSAGES[$error[0]]["string"], ["arg" => $error[1]], false);
$alerttype = MESSAGES[$error[0]]["type"];
break;
default:
$alert = lang(MESSAGES["generic_op_error"]["string"], false);
$alerttype = MESSAGES["generic_op_error"]["type"];
}
} else {
session_destroy();
header('Location: index.php');
die();
}
}
?>
<!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>
/**
* 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">
<link href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/font-awesome.min.css" rel="stylesheet">
<link href="static/css/material-color/material-color.min.css" rel="stylesheet">
<link href="static/css/app.css" rel="stylesheet">
<?php if (RECAPTCHA_ENABLED) { ?>
<script src='https://www.google.com/recaptcha/api.js'></script>
<?php } ?>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-4 col-sm-offset-3 col-md-offset-4 col-lg-offset-4">
<div>
<?php
if (SHOW_ICON == "both" || SHOW_ICON == "index") {
?>
<img class="img-responsive banner-image" src="static/img/logo.svg" />
<?php } ?>
</div>
<div class="panel panel-orange">
<div class="panel-heading">
<h3 class="panel-title"><?php lang("sign in"); ?></h3>
</div>
<div class="panel-body">
<form action="" method="POST">
<?php
if (!is_empty($alert)) {
$alerttype = isset($alerttype) ? $alerttype : "danger";
?>
<div class="alert alert-<?php echo $alerttype ?>">
<?php
switch ($alerttype) {
case "danger":
$alerticon = "times";
break;
case "warning":
$alerticon = "exclamation-triangle";
break;
case "info":
$alerticon = "info-circle";
break;
case "success":
$alerticon = "check";
break;
default:
$alerticon = "square-o";
}
?>
<i class="fa fa-fw fa-<?php echo $alerticon ?>"></i> <?php echo $alert ?>
</div>
<?php
}
<title><?php echo $SETTINGS['site_title']; ?></title>
if (!$multiauth && !$change_password) {
?>
<input type="text" class="form-control" name="username" placeholder="<?php lang("username"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="password" class="form-control" name="password" placeholder="<?php lang("password"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
<?php if (RECAPTCHA_ENABLED) { ?>
<div class="g-recaptcha" data-sitekey="<?php echo RECAPTCHA_SITE_KEY; ?>"></div>
<br />
<?php } ?>
<input type="hidden" name="progress" value="1" />
<?php
} else if ($multiauth) {
?>
<div class="alert alert-info">
<?php lang("2fa prompt"); ?>
</div>
<input type="text" class="form-control" name="authcode" placeholder="<?php lang("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 lang("continue"); ?>
</button>
</form>
</div>
<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;
}
.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 $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 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.2.1.min.js"></script>
<script src="static/js/bootstrap.min.js"></script>
</body>
</html>
</div>
<?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());
}
}

View File

@ -1,98 +0,0 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
$STRINGS = [
"sign in" => "Sign In",
"username" => "Username",
"password" => "Password",
"continue" => "Continue",
"authcode" => "Authentication code",
"2fa prompt" => "Enter the six-digit code from your mobile authenticator "
. "app.",
"2fa incorrect" => "Authentication code incorrect.",
"login incorrect" => "Login incorrect.",
"login successful" => "Login successful.",
"login error" => "There was a server problem. Try again later.",
"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.",
"password on 500 list" => "The given password is ranked number {arg} out "
. "of the 500 most common passwords. Try a different one.",
"welcome user" => "Welcome, {user}!",
"change password" => "Change password",
"account security" => "Account security",
"security options" => "Security options",
"account options" => "Account options",
"options" => "Options",
"sign out" => "Sign out",
"settings" => "Settings",
"account" => "Account",
"404 error" => "404 Error",
"page not found" => "Page not found.",
"current password incorrect" => "The current password is incorrect. "
. "Try again.",
"new password mismatch" => "The new passwords did not match. Try again.",
"weak password" => "Password does not meet requirements.",
"password updated" => "Password updated successfully.",
"setup 2fa" => "Setup 2-factor authentication",
"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 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.",
"confirm 2fa" => "Finish setup",
"invalid parameters" => "Invalid request parameters.",
"ldap server error" => "The LDAP server returned an error: {arg}",
"user does not exist" => "User does not exist.",
"captcha error" => "There was a problem with the CAPTCHA (robot test). "
. "Try again.",
"home" => "Home",
"ldap error" => "LDAP error: {error}",
"old and new passwords match" => "Your current and new passwords are the "
. "same.",
"generic op error" => "An unknown error occurred. Try again later.",
"password complexity insufficent" => "The new password does not meet the "
. "minumum requirements defined by your system administrator.",
"error loading widget" => "There was a problem loading this app.",
"open app" => "Open App",
"sign in again" => "Please sign in again to continue.",
"login failed try on web" => "There is a problem with your account. Visit "
. "AccountHub via a web browser for more information.",
"mobile login disabled" => "Mobile login has been disabled by your system "
. "administrator. Contact technical support for more information.",
"admin alert email subject" => "Alert: User login notification",
"admin alert email message" => "You (or another administrator) requested to"
. " be notified when user \"{username}\" logged in, an event which happened"
. " just now."
. "\r\n"
. "\r\nUsername: \t{username}"
. "\r\nApplication: \t{appname}"
. "\r\nDate/Time: \t{datetime}"
. "\r\nIP address: \t{ipaddr}"
. "\r\n"
. "\r\nThese notifications can be disabled by editing the user in "
. "ManagePanel.",
"enter otp code" => "Enter 6-digit code",
"secret key" => "Secret key",
"label" => "Label",
"issuer" => "Issuer",
];

17
langs/en/2fa.json Normal file
View File

@ -0,0 +1,17 @@
{
"setup 2fa": "Setup 2-factor authentication",
"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 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.",
"confirm 2fa": "Finish setup",
"enter otp code": "Enter 6-digit code",
"secret key": "Secret key",
"label": "Label",
"issuer": "Issuer",
"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."
}

5
langs/en/api.json Normal file
View File

@ -0,0 +1,5 @@
{
"user does not exist": "User does not exist.",
"group does not exist": "Group does not exist.",
"login successful": "Login successful."
}

View File

@ -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."
}

13
langs/en/cards.json Normal file
View File

@ -0,0 +1,13 @@
{
"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",
"Manage users, permissions, and security": "Manage users, permissions, and security",
"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 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"
}

7
langs/en/core.json Normal file
View File

@ -0,0 +1,7 @@
{
"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}"
}

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

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

4
langs/en/ldap.json Normal file
View File

@ -0,0 +1,4 @@
{
"ldap server error": "The LDAP server returned an error: {arg}",
"ldap error": "LDAP error: {error}"
}

25
langs/en/login.json Normal file
View File

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

View File

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

13
langs/en/password.json Normal file
View File

@ -0,0 +1,13 @@
{
"password on 500 list": "The given password is ranked number {arg} out of the 500 most common passwords. Try a different one.",
"change password": "Change password",
"current password incorrect": "The current password is incorrect. Try again.",
"new password mismatch": "The new passwords did not match. Try again.",
"weak password": "Password does not meet requirements.",
"password updated": "Password updated successfully.",
"current password": "Current password",
"new password": "New password",
"confirm password": "New password (again)",
"password complexity insufficent": "The new password does not meet the minumum requirements defined by your system administrator.",
"old and new passwords match": "Your current and new passwords are the same."
}

9
langs/en/pin.json Normal file
View File

@ -0,0 +1,9 @@
{
"pin explanation": "Change or set a login PIN for the Station kiosk Quick Access. PIN codes must be between one and eight digits.",
"change pin": "Change PIN",
"new pin": "New PIN",
"confirm pin": "New PIN (again)",
"pin updated": "PIN updated.",
"new pin mismatch": "The new PINs don't match each other.",
"invalid pin format": "PIN codes must be numeric and between one and eight digits in length."
}

7
langs/en/security.json Normal file
View File

@ -0,0 +1,7 @@
{
"sign in again": "Please sign in again to continue.",
"login failed try on web": "There is a problem with your account. Visit AccountHub via a web browser for more information.",
"mobile login disabled": "Mobile login has been disabled by your system administrator. Contact technical support for more information.",
"admin alert email subject": "Alert: User login notification",
"admin alert email message": "You (or another administrator) requested to be notified when user \"{username}\" logged in, an event which happened just now.\r\n\r\nUsername: \t{username}\r\nApplication: \t{appname}\r\nDate\/Time: \t{datetime}\r\nIP address: \t{ipaddr}\r\n\r\nThese notifications can be disabled by editing the user in ManagePanel."
}

15
langs/en/signup.json Normal file
View File

@ -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."
}

17
langs/en/sync.json Normal file
View File

@ -0,0 +1,17 @@
{
"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 {site_name} with the Netsyms mobile app.",
"generate sync": "Create new sync code",
"active sync codes": "Active codes",
"no active codes": "No active codes.",
"done adding sync code": "Done adding code",
"manual setup": "Manual Setup:",
"sync key": "Sync key:",
"url": "URL:",
"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"
}

9
langs/en/titles.json Normal file
View File

@ -0,0 +1,9 @@
{
"account security": "Account security",
"security options": "Security options",
"account options": "Account options",
"sync": "Sync settings",
"settings": "Settings",
"account": "Account",
"Home": "Home"
}

View File

@ -45,10 +45,6 @@ define("MESSAGES", [
"string" => "account state error",
"type" => "danger"
],
"ldap_error" => [
"string" => "ldap server error",
"type" => "danger"
],
"passwords_same" => [
"string" => "old and new passwords match",
"type" => "danger"
@ -60,5 +56,25 @@ define("MESSAGES", [
"generic_op_error" => [
"string" => "generic op error",
"type" => "danger"
],
"pin_updated" => [
"string" => "pin updated",
"type" => "success"
],
"new_pin_mismatch" => [
"string" => "new pin mismatch",
"type" => "danger"
],
"invalid_pin_format" => [
"string" => "invalid pin format",
"type" => "danger"
],
"notification_deleted" => [
"string" => "Notification deleted.",
"type" => "success"
],
"feed_key_reset" => [
"string" => "Feed key reset.",
"type" => "success"
]
]);

35
lib/Exceptions.lib.php Normal file
View File

@ -0,0 +1,35 @@
<?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 IncorrectPasswordException extends Exception {
public function __construct(string $message = "Incorrect password.", int $code = 0, \Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
}
class WeakPasswordException extends Exception {
public function __construct(string $message = "Password is weak or compromised.", int $code = 0, \Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
}
class PasswordMatchException extends Exception {
public function __construct(string $message = "Old and new passwords are identical", int $code = 0, \Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
}
class PasswordMismatchException extends Exception {
public function __construct(string $message = "Passwords do not match", int $code = 0, \Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
}

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

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

135
lib/IPUtils.lib.php Normal file
View File

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

72
lib/Log.lib.php Normal file
View File

@ -0,0 +1,72 @@
<?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 Log {
/**
*
* @global $database
* @param int/LogType $type Either an integer (as defined by the constants in class LogType) or a LogType object.
* @param int/User $user Either a UID number or a User object.
* @param string $data Extra data to include in the log, in addition to the timestamp, log type, user, and IP address.
*/
public static function insert($type, $user, string $data = "") {
global $database;
// find IP address
$ip = IPUtils::getClientIP();
if (gettype($type) == "object" && is_a($type, "LogType")) {
$type = $type->getType();
}
if (is_a($user, "User")) {
$uid = $user->getUID();
} else if (gettype($user) == "integer") {
$uid = $user;
} else {
$uid = null;
}
$database->insert("authlog", ['logtime' => date("Y-m-d H:i:s"), 'logtype' => $type, 'uid' => $uid, 'ip' => $ip, 'otherdata' => $data]);
}
}
class LogType {
const LOGIN_OK = 1;
const LOGIN_FAILED = 2;
const PASSWORD_CHANGED = 3;
const API_LOGIN_OK = 4;
const API_LOGIN_FAILED = 5;
const BAD_2FA = 6;
const API_BAD_2FA = 7;
const BAD_CAPTCHA = 8;
const ADDED_2FA = 9;
const REMOVED_2FA = 10;
const LOGOUT = 11;
const API_AUTH_OK = 12;
const API_AUTH_FAILED = 13;
const API_BAD_KEY = 14;
const LOG_CLEARED = 15;
const USER_REMOVED = 16;
const USER_ADDED = 17;
const USER_EDITED = 18;
const MOBILE_LOGIN_OK = 19;
const MOBILE_LOGIN_FAILED = 20;
const MOBILE_BAD_KEY = 21;
private $type;
function __construct(int $type) {
$this->type = $type;
}
public function getType(): int {
return $type;
}
}

71
lib/Login.lib.php Normal file
View File

@ -0,0 +1,71 @@
<?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 Login {
const BAD_USERPASS = 1;
const BAD_2FA = 2;
const ACCOUNT_DISABLED = 3;
const LOGIN_OK = 4;
public static function auth(string $username, string $password, string $twofa = ""): int {
global $database;
$username = strtolower($username);
$user = User::byUsername($username);
if (!$user->exists()) {
return Login::BAD_USERPASS;
}
if (!$user->checkPassword($password)) {
return Login::BAD_USERPASS;
}
if ($user->has2fa()) {
if (!$user->check2fa($twofa)) {
return Login::BAD_2FA;
}
}
switch ($user->getStatus()->get()) {
case AccountStatus::TERMINATED:
return Login::BAD_USERPASS;
case AccountStatus::LOCKED_OR_DISABLED:
return Login::ACCOUNT_DISABLED;
case AccountStatus::NORMAL:
default:
return Login::LOGIN_OK;
}
return Login::LOGIN_OK;
}
public static function verifyCaptcha(string $session, string $answer, string $url): bool {
$data = [
'session_id' => $session,
'answer_id' => $answer,
'action' => "verify"
];
$options = [
'http' => [
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($data)
]
];
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
$resp = json_decode($result, TRUE);
if (!$resp['result']) {
return false;
} else {
return true;
}
}
}

33
lib/LoginKey.lib.php Normal file
View File

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

99
lib/Notifications.lib.php Normal file
View File

@ -0,0 +1,99 @@
<?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 Notifications {
/**
* Add a new notification.
* @global $database
* @param User $user
* @param string $title
* @param string $content
* @param string $timestamp If left empty, the current date and time will be used.
* @param string $url
* @param bool $sensitive If true, the notification is marked as containing sensitive content, and the $content might be hidden on lockscreens and other non-secure places.
* @return int The newly-created notification ID.
* @throws Exception
*/
public static function add(User $user, string $title, string $content, string $timestamp = "", string $url = "", bool $sensitive = false): int {
global $database, $Strings;
if ($user->exists()) {
if (empty($title) || empty($content)) {
throw new Exception($Strings->get("invalid parameters", false));
}
$timestamp = date("Y-m-d H:i:s");
if (!empty($timestamp)) {
$timestamp = date("Y-m-d H:i:s", strtotime($timestamp));
}
$database->insert('notifications', ['uid' => $user->getUID(), 'timestamp' => $timestamp, 'title' => $title, 'content' => $content, 'url' => $url, 'seen' => 0, 'sensitive' => $sensitive]);
return $database->id() * 1;
}
throw new Exception($Strings->get("user does not exist", false));
}
/**
* 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, bool $all = true) {
global $database, $Strings;
if ($user->exists()) {
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);
$notifications[$i]['sensitive'] = ($notifications[$i]['sensitive'] == "1" ? true : false);
}
return $notifications;
}
throw new Exception($Strings->get("user does not exist", false));
}
/**
* Mark the notification identified by $id as read.
* @global $database
* @global $Strings
* @param User $user
* @param int $id
* @throws Exception
*/
public static function read(User $user, int $id) {
global $database, $Strings;
if ($user->exists()) {
if ($database->has('notifications', ['AND' => ['uid' => $user->getUID(), 'notificationid' => $id]])) {
$database->update('notifications', ['seen' => 1], ['AND' => ['uid' => $user->getUID(), 'notificationid' => $id]]);
return true;
}
throw new Exception($Strings->get("invalid parameters", false));
}
throw new Exception($Strings->get("user does not exist", false));
}
public static function delete(User $user, int $id) {
global $database, $Strings;
if ($user->exists()) {
if ($database->has('notifications', ['AND' => ['uid' => $user->getUID(), 'notificationid' => $id]])) {
$database->delete('notifications', ['AND' => ['uid' => $user->getUID(), 'notificationid' => $id]]);
return true;
}
throw new Exception($Strings->get("invalid parameters", false));
}
throw new Exception($Strings->get("user does not exist", false));
}
}

31
lib/RandomString.lib.php Normal file
View File

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

19
lib/Session.lib.php Normal file
View File

@ -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/.
*/
class Session {
public static function start(User $user) {
$_SESSION['username'] = $user->getUsername();
$_SESSION['uid'] = $user->getUID();
$_SESSION['email'] = $user->getEmail();
$_SESSION['realname'] = $user->getName();
$_SESSION['loggedin'] = true;
}
}

122
lib/Strings.lib.php Normal file
View File

@ -0,0 +1,122 @@
<?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/.
*/
/**
* Provides translated language strings.
*/
class Strings {
private $language = "en";
private $strings = [];
public function __construct($language = "en") {
if (!preg_match("/[a-zA-Z\_\-]+/", $language)) {
throw new Exception("Invalid language code $language");
}
$this->load("en");
if ($language == "en") {
return;
}
if (file_exists(__DIR__ . "/../langs/$language/")) {
$this->language = $language;
$this->load($language);
} else {
trigger_error("Language $language could not be found.", E_USER_WARNING);
}
}
/**
* Load all JSON files for the specified language.
* @param string $language
*/
private function load(string $language) {
$files = glob(__DIR__ . "/../langs/$language/*.json");
foreach ($files as $file) {
$strings = json_decode(file_get_contents($file), true);
foreach ($strings as $key => $val) {
if (array_key_exists($key, $this->strings)) {
trigger_error("Language key \"$key\" is defined more than once.", E_USER_WARNING);
}
$this->strings[$key] = $val;
}
}
}
/**
* Add language strings dynamically.
* @param array $strings ["key" => "value", ...]
*/
public function addStrings(array $strings) {
foreach ($strings as $key => $val) {
$this->strings[$key] = $val;
}
}
/**
* I18N string getter. If the key isn't found, it outputs the key itself.
* @param string $key
* @param bool $echo True to echo the result, false to return it. Default is true.
* @return string
*/
public function get(string $key, bool $echo = true): string {
$str = $key;
if (array_key_exists($key, $this->strings)) {
$str = $this->strings[$key];
} else {
trigger_error("Language key \"$key\" does not exist in " . $this->language, E_USER_WARNING);
}
if ($echo) {
echo $str;
}
return $str;
}
/**
* I18N string getter (with builder). If the key doesn't exist, outputs the key itself.
* @param string $key
* @param array $replace key-value array of replacements.
* If the string value is "hello {abc}" and you give ["abc" => "123"], the
* result will be "hello 123".
* @param bool $echo True to echo the result, false to return it. Default is true.
* @return string
*/
public function build(string $key, array $replace, bool $echo = true): string {
$str = $key;
if (array_key_exists($key, $this->strings)) {
$str = $this->strings[$key];
} else {
trigger_error("Language key \"$key\" does not exist in " . $this->language, E_USER_WARNING);
}
foreach ($replace as $find => $repl) {
$str = str_replace("{" . $find . "}", $repl, $str);
}
if ($echo) {
echo $str;
}
return $str;
}
/**
* Builds and returns a JSON key:value string for the supplied array of keys.
* @param array $keys ["key1", "key2", ...]
*/
public function getJSON(array $keys): string {
$strings = [];
foreach ($keys as $k) {
$strings[$k] = $this->get($k, false);
}
return json_encode($strings);
}
}

329
lib/User.lib.php Normal file
View File

@ -0,0 +1,329 @@
<?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/.
*/
use Base32\Base32;
use OTPHP\TOTP;
class User {
private $uid = null;
private $username;
private $passhash;
private $email;
private $realname;
private $authsecret;
private $has2fa = false;
private $exists = false;
private $apppasswords = [];
public function __construct(int $uid, string $username = "") {
global $database;
if ($database->has('accounts', ['AND' => ['uid' => $uid, 'deleted' => false]])) {
$this->uid = $uid;
$user = $database->get('accounts', ['username', 'password', 'email', 'realname', 'authsecret'], ['uid' => $uid]);
$this->username = $user['username'];
$this->passhash = $user['password'];
$this->email = $user['email'];
$this->realname = $user['realname'];
$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;
}
}
public static function byUsername(string $username): User {
global $database;
$username = strtolower($username);
if ($database->has('accounts', ['AND' => ['username' => $username, 'deleted' => false]])) {
$uid = $database->get('accounts', 'uid', ['username' => $username]);
return new self($uid * 1);
}
return new self(-1, $username);
}
/**
* Add a user to the system. /!\ Assumes input is OK /!\
* @param string $username Username, saved in lowercase.
* @param string $password Password, will be hashed before saving.
* @param string $realname User's real legal name
* @param string $email User's email address.
* @param string $phone1 Phone number #1
* @param string $phone2 Phone number #2
* @param int $type Account type
* @return int The new user's ID number in the database.
*/
public static function add(string $username, string $password, string $realname, string $email = null, string $phone1 = "", string $phone2 = "", int $type = 1): int {
global $database;
$database->insert('accounts', [
'username' => strtolower($username),
'password' => (is_null($password) ? null : password_hash($password, PASSWORD_BCRYPT)),
'realname' => $realname,
'email' => $email,
'phone1' => $phone1,
'phone2' => $phone2,
'acctstatus' => 1,
'accttype' => $type
]);
return $database->id();
}
public function exists(): bool {
return $this->exists;
}
public function has2fa(): bool {
return $this->has2fa;
}
function getUsername() {
return $this->username;
}
function getUID() {
return $this->uid;
}
function getEmail() {
return $this->email;
}
function getName() {
return $this->realname;
}
/**
* Check the given plaintext password against the stored hash.
* @param string $password
* @return bool
*/
function checkPassword(string $password): bool {
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
* @param string $old The current password
* @param string $new The new password
* @param string $new2 New password again
* @throws PasswordMatchException
* @throws PasswordMismatchException
* @throws IncorrectPasswordException
* @throws WeakPasswordException
*/
function changePassword(string $old, string $new, string $new2) {
global $database, $SETTINGS;
if ($old == $new) {
throw new PasswordMatchException();
}
if ($new != $new2) {
throw new PasswordMismatchException();
}
if (!$this->checkPassword($old)) {
throw new IncorrectPasswordException();
}
require_once __DIR__ . "/worst_passwords.php";
$passrank = checkWorst500List($new);
if ($passrank !== FALSE) {
throw new WeakPasswordException();
}
if (strlen($new) < $SETTINGS['min_password_length']) {
throw new WeakPasswordException();
}
$database->update('accounts', ['password' => password_hash($new, PASSWORD_DEFAULT), 'acctstatus' => 1], ['uid' => $this->uid]);
Log::insert(LogType::PASSWORD_CHANGED, $this);
return true;
}
function check2fa(string $code): bool {
if (!$this->has2fa) {
return true;
}
$totp = new TOTP(null, $this->authsecret);
$time = time();
if ($totp->verify($code, $time)) {
return true;
}
if ($totp->verify($code, $time - 30)) {
return true;
}
if ($totp->verify($code, $time + 30)) {
return true;
}
return false;
}
/**
* Generate a TOTP secret for the given 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($SETTINGS['system_name']);
return $totp->getProvisioningUri();
}
/**
* Save a TOTP secret for the user.
* @global $database $database
* @param string $username
* @param string $secret
*/
function save2fa(string $secret) {
global $database;
$database->update('accounts', ['authsecret' => $secret], ['username' => $this->username]);
}
/**
* Check if the given username has the given permission (or admin access)
* @global $database $database
* @param string $code
* @return boolean TRUE if the user has the permission (or admin access), else FALSE
*/
function hasPermission(string $code): bool {
global $database;
return $database->has('assigned_permissions', [
'[>]permissions' => [
'permid' => 'permid'
]
], ['AND' => ['OR' => ['permcode #code' => $code, 'permcode #admin' => 'ADMIN'], 'uid' => $this->uid]]) === TRUE;
}
/**
* Get the account status.
* @return \AccountStatus
*/
function getStatus(): AccountStatus {
global $database;
$statuscode = $database->get('accounts', 'acctstatus', ['uid' => $this->uid]);
return new AccountStatus($statuscode);
}
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 (empty($SETTINGS["email"]["from"]) || filter_var($SETTINGS["email"]["from"], FILTER_VALIDATE_EMAIL) === FALSE) {
return "invalid_from_email";
}
$mail = new PHPMailer;
if ($SETTINGS['debug']) {
$mail->SMTPDebug = 2;
}
if ($SETTINGS['email']['use_smtp']) {
$mail->isSMTP();
$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,
'verify_peer_name' => false,
'allow_self_signed' => true
)
);
}
}
$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" => IPUtils::getClientIP(), "appname" => $appname], false);
if (!$mail->send()) {
return $mail->ErrorInfo;
}
return true;
}
}
class AccountStatus {
const NORMAL = 1;
const LOCKED_OR_DISABLED = 2;
const CHANGE_PASSWORD = 3;
const TERMINATED = 4;
const ALERT_ON_ACCESS = 5;
private $status;
public function __construct(int $status) {
$this->status = $status;
}
/**
* Get the account status/state as an integer.
* @return int
*/
public function get(): int {
return $this->status;
}
/**
* Get the account status/state as a string representation.
* @return string
*/
public function getString(): string {
switch ($this->status) {
case self::NORMAL:
return "NORMAL";
case self::LOCKED_OR_DISABLED:
return "LOCKED_OR_DISABLED";
case self::CHANGE_PASSWORD:
return "CHANGE_PASSWORD";
case self::TERMINATED:
return "TERMINATED";
case self::ALERT_ON_ACCESS:
return "ALERT_ON_ACCESS";
default:
return "OTHER_" . $this->status;
}
}
}

View File

@ -1,542 +0,0 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Authentication and account functions
*/
use Base32\Base32;
use OTPHP\TOTP;
use LdapTools\LdapManager;
use LdapTools\Object\LdapObjectType;
$ldap = new LdapManager($ldap_config);
////////////////////////////////////////////////////////////////////////////////
// Account handling //
////////////////////////////////////////////////////////////////////////////////
/**
* Add a user to the system. /!\ Assumes input is OK /!\
* @param string $username Username, saved in lowercase.
* @param string $password Password, will be hashed before saving.
* @param string $realname User's real legal name
* @param string $email User's email address.
* @param string $phone1 Phone number #1
* @param string $phone2 Phone number #2
* @param int $type Account type
* @return int The new user's ID number in the database.
*/
function adduser($username, $password, $realname, $email = null, $phone1 = "", $phone2 = "", $type = 1) {
global $database;
$database->insert('accounts', [
'username' => strtolower($username),
'password' => (is_null($password) ? null : encryptPassword($password)),
'realname' => $realname,
'email' => $email,
'phone1' => $phone1,
'phone2' => $phone2,
'acctstatus' => 1,
'accttype' => $type
]);
//var_dump($database->error());
return $database->id();
}
/**
* Change the password for the current user.
* @global $database $database
* @global LdapManager $ldap
* @param string $old The current password
* @param string $new The new password
* @param string $new2 New password again
* @param [string] $error If the function returns false, this will have an array
* with a message ID from `lang/messages.php` and (depending on the message) an
* extra string for that message.
* @return boolean true if the password is changed, else false
*/
function change_password($old, $new, $new2, &$error) {
global $database, $ldap;
// make sure the new password isn't the same as the current one
if ($old == $new) {
$error = ["passwords_same"];
return false;
}
// Make sure the new passwords are the same
if ($new != $new2) {
$error = ["new_password_mismatch"];
return false;
}
// check the current password
$login_ok = authenticate_user($_SESSION['username'], $old, $errmsg, $errcode);
// Allow login if the error is due to expired password
if (!$login_ok && ($errcode == LdapTools\Connection\ADResponseCodes::ACCOUNT_PASSWORD_EXPIRED || $errcode == LdapTools\Connection\ADResponseCodes::ACCOUNT_PASSWORD_MUST_CHANGE)) {
$login_ok = true;
}
if ($login_ok) {
// Check the new password and make sure it's not stupid
require_once __DIR__ . "/worst_passwords.php";
$passrank = checkWorst500List($new);
if ($passrank !== FALSE) {
$error = ["password_500", $passrank];
return false;
}
if (strlen($new) < MIN_PASSWORD_LENGTH) {
$error = ["weak_password"];
return false;
}
// Figure out how to change the password, then do it
$acctloc = account_location($_SESSION['username']);
if ($acctloc == "LOCAL") {
$database->update('accounts', ['password' => encryptPassword($new), 'acctstatus' => 1], ['uid' => $_SESSION['uid']]);
$_SESSION['password'] = $new;
insertAuthLog(3, $_SESSION['uid']);
return true;
} else if ($acctloc == "LDAP") {
try {
$repository = $ldap->getRepository(LdapObjectType::USER);
$user = $repository->findOneByUsername($_SESSION['username']);
$user->setPassword($new);
$user->setpasswordMustChange(false);
$ldap->persist($user);
$database->update('accounts', ['acctstatus' => 1], ['uid' => $_SESSION['uid']]);
insertAuthLog(3, $_SESSION['uid']);
$_SESSION['password'] = $new;
return true;
} catch (\Exception $e) {
// Stupid password complexity BS error
if (strpos($e->getMessage(), "DSID-031A11E5") !== FALSE) {
$error = ["password_complexity"];
return false;
}
$error = ["ldap_error", $e->getMessage()];
return false;
}
}
$error = ["account_state_error"];
return false;
}
$error = ["old_password_mismatch"];
return false;
}
/**
* Get where a user's account actually is.
* @param string $username
* @return string "LDAP", "LOCAL", "LDAP_ONLY", or "NONE".
*/
function account_location($username) {
global $database;
$username = strtolower($username);
$user_exists_local = user_exists_local($username);
if (!$user_exists_local && !LDAP_ENABLED) {
return "NONE";
}
if ($user_exists_local) {
$userinfo = $database->select('accounts', ['password'], ['username' => $username])[0];
// if password empty, it's an LDAP user
if (!is_empty($userinfo['password'])) {
return "LOCAL";
} else if (is_empty($userinfo['password']) && LDAP_ENABLED) {
return "LDAP";
} else {
return "NONE";
}
} else {
if (user_exists_ldap($username)) {
return "LDAP_ONLY";
} else {
return "NONE";
}
}
}
/**
* Checks the given credentials against the database.
* @param string $username
* @param string $password
* @return boolean True if OK, else false
*/
function authenticate_user($username, $password, &$errormsg = null, &$errorcode = null) {
global $database;
global $ldap;
$username = strtolower($username);
if (is_empty($username) || is_empty($password)) {
return "NONE";
}
$loc = account_location($username, $password);
switch ($loc) {
case "LOCAL":
$hash = $database->select('accounts', ['password'], ['username' => $username, "LIMIT" => 1])[0]['password'];
return (comparePassword($password, $hash));
case "LDAP":
return authenticate_user_ldap($username, $password, $errormsg, $errorcode) === TRUE;
case "LDAP_ONLY":
// Authenticate with LDAP and create database account
try {
if (authenticate_user_ldap($username, $password, $errormsg, $errorcode) === TRUE) {
$user = $ldap->getRepository('user')->findOneByUsername($username);
adduser($user->getUsername(), null, $user->getName(), ($user->hasEmailAddress() ? $user->getEmailAddress() : null), "", "", 2);
return true;
}
return false;
} catch (Exception $e) {
$errormsg = $e->getMessage();
return false;
}
default:
return false;
}
}
function user_exists($username) {
return account_location(strtolower($username)) !== "NONE";
}
/**
* Check if a username exists in the local database.
* @param String $username
*/
function user_exists_local($username) {
global $database;
$username = strtolower($username);
return $database->has('accounts', ['username' => $username]) === TRUE;
}
/**
* Get the account status: NORMAL, TERMINATED, LOCKED_OR_DISABLED,
* CHANGE_PASSWORD, ALERT_ON_ACCESS, or OTHER
* @global $database $database
* @param string $username
* @param string $password
* @return string
*/
function get_account_status($username, &$error = null) {
global $database;
$username = strtolower($username);
$loc = account_location($username);
if ($loc == "LOCAL") {
$statuscode = $database->select('accounts', [
'[>]acctstatus' => [
'acctstatus' => 'statusid'
]
], [
'accounts.acctstatus',
'acctstatus.statuscode'
], [
'username' => $username,
"LIMIT" => 1
]
)[0]['statuscode'];
return $statuscode;
} else if ($loc == "LDAP" || $loc == "LDAP_ONLY") {
return get_account_status_ldap($username, $error);
} else {
// account isn't setup properly
return "OTHER";
}
}
/**
* Check if the given username has the given permission (or admin access)
* @global $database $database
* @param string $username
* @param string $permcode
* @return boolean TRUE if the user has the permission (or admin access), else FALSE
*/
function account_has_permission($username, $permcode) {
global $database;
return $database->has('assigned_permissions', [
'[>]accounts' => [
'uid' => 'uid'
],
'[>]permissions' => [
'permid' => 'permid'
]
], ['AND' => ['OR' => ['permcode #code' => $permcode, 'permcode #admin' => 'ADMIN'], 'username' => $username]]) === TRUE;
}
////////////////////////////////////////////////////////////////////////////////
// Login handling //
////////////////////////////////////////////////////////////////////////////////
/**
* Setup $_SESSION values to log in a user
* @param string $username
*/
function doLoginUser($username, $password) {
global $database;
$username = strtolower($username);
$userinfo = $database->select('accounts', ['email', 'uid', 'realname'], ['username' => $username])[0];
$_SESSION['username'] = $username;
$_SESSION['uid'] = $userinfo['uid'];
$_SESSION['email'] = $userinfo['email'];
$_SESSION['realname'] = $userinfo['realname'];
$_SESSION['password'] = $password; // needed for accessing data in other apps
$_SESSION['loggedin'] = true;
}
/**
* Send an alert email to the system admin
*
* Used when an account with the status ALERT_ON_ACCESS logs in
* @param String $username the account username
* @return Mixed TRUE if successful, error string if not
*/
function sendLoginAlertEmail($username, $appname = SITE_TITLE) {
if (is_empty(ADMIN_EMAIL) || filter_var(ADMIN_EMAIL, FILTER_VALIDATE_EMAIL) === FALSE) {
return "invalid_to_email";
}
if (is_empty(FROM_EMAIL) || filter_var(FROM_EMAIL, FILTER_VALIDATE_EMAIL) === FALSE) {
return "invalid_from_email";
}
$username = strtolower($username);
$mail = new PHPMailer;
if (DEBUG) {
$mail->SMTPDebug = 2;
}
if (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->SMTPOptions = array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true
)
);
}
}
$mail->setFrom(FROM_EMAIL, 'Account Alerts');
$mail->addAddress(ADMIN_EMAIL, "System Admin");
$mail->isHTML(false);
$mail->Subject = lang("admin alert email subject", false);
$mail->Body = lang2("admin alert email message", ["username" => $username, "datetime" => date("Y-m-d H:i:s"), "ipaddr" => getClientIP(), "appname" => $appname], false);
if (!$mail->send()) {
return $mail->ErrorInfo;
}
return TRUE;
}
function insertAuthLog($type, $uid = null, $data = "") {
global $database;
// find IP address
$ip = getClientIP();
$database->insert("authlog", ['logtime' => date("Y-m-d H:i:s"), 'logtype' => $type, 'uid' => $uid, 'ip' => $ip, 'otherdata' => $data]);
}
function verifyReCaptcha($response) {
try {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', "https://www.google.com/recaptcha/api/siteverify", [
'form_params' => [
'secret' => RECAPTCHA_SECRET_KEY,
'response' => $response
]
]);
if ($response->getStatusCode() != 200) {
return false;
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['success'] === true) {
return true;
} else {
return false;
}
} catch (Exception $e) {
return false;
}
}
////////////////////////////////////////////////////////////////////////////////
// LDAP handling //
////////////////////////////////////////////////////////////////////////////////
/**
* Checks the given credentials against the LDAP server.
* @param string $username
* @param string $password
* @return mixed True if OK, else false or the error code from the server
*/
function authenticate_user_ldap($username, $password, &$errormsg = null, &$errorcode = null) {
global $ldap;
if (is_empty($username) || is_empty($password)) {
return false;
}
$username = strtolower($username);
try {
$msg = "";
$code = 0;
if ($ldap->authenticate($username, $password, $msg, $code) === TRUE) {
$errormsg = $msg;
$errorcode = $code;
return true;
} else {
$errormsg = $msg;
$errorcode = $code;
return $msg;
}
} catch (Exception $e) {
$errormsg = $e->getMessage();
return $e->getMessage();
}
}
/**
* Check if a username exists on the LDAP server.
* @global type $ldap_config
* @param type $username
* @return boolean true if yes, else false
*/
function user_exists_ldap($username) {
global $ldap;
try {
$username = strtolower($username);
$lqb = $ldap->buildLdapQuery();
$result = $lqb->fromUsers()
->where(['username' => $username])
->getLdapQuery()
->getResult();
if (count($result) > 0) {
return true;
}
return false;
} catch (Exception $e) {
return false;
}
}
function get_account_status_ldap($username, &$error = null) {
global $ldap;
try {
$username = strtolower($username);
$normal = $ldap->buildLdapQuery()
->fromUsers()
->where(['enabled' => true, 'passwordMustChange' => false, 'locked' => false, 'disabled' => false, 'username' => $username])
->getLdapQuery()
->getResult();
if (count($normal) == 1) {
return "NORMAL";
}
$disabled = $ldap->buildLdapQuery()
->fromUsers()
->where(['disabled' => true, 'username' => $username])
->getLdapQuery()
->getResult();
$locked = $ldap->buildLdapQuery()
->fromUsers()
->where(['locked' => true, 'username' => $username])
->getLdapQuery()
->getResult();
if (count($disabled) == 1 || count($locked) == 1) {
return "LOCKED_OR_DISABLED";
}
$passwordExpired = $ldap->buildLdapQuery()
->fromUsers()
->where(['passwordMustChange' => true, 'username' => $username])
->getLdapQuery()
->getResult();
if (count($passwordExpired) == 1) {
return "CHANGE_PASSWORD";
}
$other = $ldap->buildLdapQuery()
->fromUsers()
->where(['username' => $username])
->getLdapQuery()
->getResult();
if (count($other) == 0) {
return false;
} else {
return "OTHER";
}
} catch (Exception $e) {
$error = $e->getMessage();
return false;
}
}
////////////////////////////////////////////////////////////////////////////////
// 2-factor authentication //
////////////////////////////////////////////////////////////////////////////////
/**
* Check if a user has TOTP setup
* @global $database $database
* @param string $username
* @return boolean true if TOTP secret exists, else false
*/
function userHasTOTP($username) {
global $database;
$username = strtolower($username);
$secret = $database->select('accounts', 'authsecret', ['username' => $username])[0];
if (is_empty($secret)) {
return false;
}
return true;
}
/**
* Generate a TOTP secret for the given user.
* @param string $username
* @return string OTP provisioning URI (for generating a QR code)
*/
function newTOTP($username) {
global $database;
$username = strtolower($username);
$secret = random_bytes(20);
$encoded_secret = Base32::encode($secret);
$userdata = $database->select('accounts', ['email', 'authsecret', 'realname'], ['username' => $username])[0];
$totp = new TOTP((is_null($userdata['email']) ? $userdata['realname'] : $userdata['email']), $encoded_secret);
$totp->setIssuer(SYSTEM_NAME);
return $totp->getProvisioningUri();
}
/**
* Save a TOTP secret for the user.
* @global $database $database
* @param string $username
* @param string $secret
*/
function saveTOTP($username, $secret) {
global $database;
$username = strtolower($username);
$database->update('accounts', ['authsecret' => $secret], ['username' => $username]);
}
/**
* Verify a TOTP multiauth code
* @global $database
* @param string $username
* @param int $code
* @return boolean true if it's legit, else false
*/
function verifyTOTP($username, $code) {
global $database;
$username = strtolower($username);
$userdata = $database->select('accounts', ['email', 'authsecret'], ['username' => $username])[0];
if (is_empty($userdata['authsecret'])) {
return false;
}
$totp = new TOTP(null, $userdata['authsecret']);
return $totp->verify($code);
}

169
login/index.php Normal file
View File

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

View File

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

15
login/parts/footer.php Normal file
View File

@ -0,0 +1,15 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
?>
</div>
</div>
</div>
</div>
</div>
<script src="../static/js/fontawesome-all.min.js"></script>

59
login/parts/header.php Normal file
View File

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

32
login/parts/password.php Normal file
View File

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

31
login/parts/totp.php Normal file
View File

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

37
login/parts/username.php Normal file
View File

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

View File

@ -10,8 +10,6 @@
require __DIR__ . "/../required.php";
require __DIR__ . "/../lib/login.php";
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
@ -20,12 +18,12 @@ if ($VARS['action'] == "ping") {
exit(json_encode(["status" => "OK"]));
}
if (MOBILE_ENABLED !== TRUE) {
exit(json_encode(["status" => "ERROR", "msg" => lang("mobile login disabled", false)]));
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."]));
}
@ -34,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
@ -58,58 +63,50 @@ switch ($VARS['action']) {
case "check_password":
// Check if the user-supplied password is valid.
engageRateLimit();
if (get_account_status($username) != "NORMAL") {
insertAuthLog(20, null, "Username: " . $username . ", Key: " . $key);
exit(json_encode(["status" => "ERROR", "msg" => lang("login failed try on web", false)]));
$user = User::byUsername($username);
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 (authenticate_user($username, $VARS['password'], $autherror)) {
$uid = $database->get("accounts", "uid", ["username" => $username]);
insertAuthLog(19, $uid, "Key: " . $key);
exit(json_encode(["status" => "OK", "uid" => $uid]));
if ($user->checkPassword($VARS['password'])) {
Log::insert(LogType::MOBILE_LOGIN_OK, $user->getUID(), "Key: " . $key);
exit(json_encode(["status" => "OK", "uid" => $user->getUID()]));
} else {
if (!is_empty($autherror)) {
insertAuthLog(20, null, "Username: " . $username . ", Key: " . $key);
exit(json_encode(["status" => "ERROR", "msg" => $autherror]));
} else {
insertAuthLog(20, null, "Username: " . $username . ", Key: " . $key);
exit(json_encode(["status" => "ERROR", "msg" => lang("login incorrect", false)]));
}
Log::insert(LogType::MOBILE_LOGIN_FAILED, null, "Username: " . $username . ", Key: " . $key);
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
}
case "user_info":
engageRateLimit();
if (get_account_status($username) != "NORMAL") {
insertAuthLog(20, null, "Username: " . $username . ", Key: " . $key);
exit(json_encode(["status" => "ERROR", "msg" => lang("login failed try on web", false)]));
$user = User::byUsername($username);
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 (authenticate_user($username, $VARS['password'], $autherror)) {
$userinfo = $database->get("accounts", ["uid", "username", "realname", "email"], ["username" => $username]);
insertAuthLog(19, $userinfo['uid'], "Key: " . $key);
if ($user->checkPassword($VARS['password'])) {
$userinfo = ["uid" => $user->getUID(), "username" => $user->getUsername(), "realname" => $user->getName(), "email" => $user->getEmail()];
Log::insert(LogType::MOBILE_LOGIN_OK, $user->getUID(), "Key: " . $key);
exit(json_encode(["status" => "OK", "info" => $userinfo]));
} else {
if (!is_empty($autherror)) {
insertAuthLog(20, null, "Username: " . $username . ", Key: " . $key);
exit(json_encode(["status" => "ERROR", "msg" => $autherror]));
} else {
insertAuthLog(20, null, "Username: " . $username . ", Key: " . $key);
exit(json_encode(["status" => "ERROR", "msg" => lang("login incorrect", false)]));
}
Log::insert(LogType::MOBILE_LOGIN_FAILED, null, "Username: " . $username . ", Key: " . $key);
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
}
case "start_session":
// Do a web login.
engageRateLimit();
if (user_exists($username)) {
if (get_account_status($username) == "NORMAL") {
if (authenticate_user($username, $VARS['password'], $autherror)) {
doLoginUser($username, $VARS['password']);
$user = User::byUsername($username);
if ($user->exists()) {
if ($user->getStatus()->get() == AccountStatus::NORMAL) {
if ($user->checkPassword($VARS['password'])) {
Session::start($user);
$_SESSION['mobile'] = true;
exit(json_encode(["status" => "OK"]));
}
}
}
insertAuthLog(20, null, "Username: " . $username . ", Key: " . $key);
exit(json_encode(["status" => "ERROR", "msg" => lang("login incorrect", false)]));
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) {
@ -117,7 +114,131 @@ switch ($VARS['action']) {
}
}
exit(json_encode(["status" => "OK", "apps" => $apps]));
case "gencode":
engageRateLimit();
$user = User::byUsername($username);
$code = "";
do {
$code = random_int(100000, 999999);
} while ($database->has("onetimekeys", ["key" => $code]));
$database->insert("onetimekeys", ["key" => $code, "uid" => $user->getUID(), "expires" => date("Y-m-d H:i:s", strtotime("+1 minute"))]);
$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."]));
}
}

9
nbproject/mplheader.txt Normal file
View File

@ -0,0 +1,9 @@
<#if licenseFirst??>
${licenseFirst}
</#if>
${licensePrefix}This Source Code Form is subject to the terms of the Mozilla Public
${licensePrefix}License, v. 2.0. If a copy of the MPL was not distributed with this
${licensePrefix}file, You can obtain one at http://mozilla.org/MPL/2.0/.
<#if licenseLast??>
${licenseLast}
</#if>

View File

@ -1,5 +1,6 @@
include.path=${php.global.include.path}
php.version=PHP_70
project.licensePath=./nbproject/mplheader.txt
source.encoding=UTF-8
src.dir=.
tags.asp=false

View File

@ -7,37 +7,27 @@
// List of pages and metadata
define("PAGES", [
"home" => [
"title" => "home",
"title" => "Home",
"navbar" => true,
"icon" => "home"
"icon" => "fas fa-home",
"styles" => [
"static/css/dock.css"
]
],
"security" => [
"title" => "account options",
"title" => "account security",
"navbar" => true,
"icon" => "cogs"
"icon" => "fas fa-lock",
"styles" => [
"static/css/qrcode.css"
]
],
"sync" => [
"title" => "sync",
"navbar" => true,
"icon" => "fas fa-sync"
],
"404" => [
"title" => "404 error",
"navbar" => false
]
]);
// Which apps to load on a given page
define("APPS", [
"home" => [
"taskfloor_tasks",
"qwikclock_inout",
"taskfloor_messages",
"inventory_link",
"account_security"
],
"security" => [
"sync_mobile",
"change_password",
"setup_2fa"
],
"404" => [
"404_error"
"title" => "404 error"
]
]);

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