Browse Source

Rewrite a lot of code, close Business/CommonBugs#3, close Business/CommonBugs#4, close #9

tags/v2.0
Skylar Ittner 11 months ago
parent
commit
b23e4bce30
74 changed files with 2855 additions and 4427 deletions
  1. 23
    20
      action.php
  2. 111
    152
      api.php
  3. 197
    0
      app.php
  4. 0
    13
      apps/404_error.php
  5. 0
    26
      apps/change_password.php
  6. 0
    27
      apps/change_pin.php
  7. 0
    79
      apps/setup_2fa.php
  8. BIN
      database.mwb
  9. 0
    0
      database_upgrade/1.0.1_2.0.sql
  10. 1
    269
      home.php
  11. 133
    138
      index.php
  12. 0
    110
      lang/en_us.php
  13. 3
    1
      langs/en/api.json
  14. 2
    1
      langs/en/sync.json
  15. 0
    0
      langs/messages.php
  16. 35
    0
      lib/Exceptions.lib.php
  17. 72
    0
      lib/Log.lib.php
  18. 71
    0
      lib/Login.lib.php
  19. 19
    0
      lib/Session.lib.php
  20. 5
    5
      lib/Strings.lib.php
  21. 307
    0
      lib/User.lib.php
  22. 0
    538
      lib/login.php
  23. 26
    36
      mobile/index.php
  24. 10
    22
      pages.php
  25. 10
    0
      pages/404.php
  26. 32
    0
      pages/home.php
  27. 127
    0
      pages/security.php
  28. 128
    0
      pages/sync.php
  29. 7
    3
      required.php
  30. 33
    97
      static/css/app.css
  31. 0
    4
      static/css/app.min.css
  32. 8
    7
      static/css/bootstrap.min.css
  33. 66
    0
      static/css/dock.css
  34. 5
    0
      static/css/fa-svg-with-js.css
  35. 0
    4
      static/css/font-awesome.min.css
  36. 15
    0
      static/css/index.css
  37. 14
    0
      static/css/qrcode.css
  38. BIN
      static/fonts/FontAwesome.otf
  39. 72
    0
      static/fonts/Roboto.css
  40. BIN
      static/fonts/Roboto_300.eot
  41. 312
    0
      static/fonts/Roboto_300.svg
  42. BIN
      static/fonts/Roboto_300.ttf
  43. BIN
      static/fonts/Roboto_300.woff
  44. BIN
      static/fonts/Roboto_300.woff2
  45. BIN
      static/fonts/Roboto_400.eot
  46. 308
    0
      static/fonts/Roboto_400.svg
  47. BIN
      static/fonts/Roboto_400.ttf
  48. BIN
      static/fonts/Roboto_400.woff
  49. BIN
      static/fonts/Roboto_400.woff2
  50. BIN
      static/fonts/Roboto_500.eot
  51. 305
    0
      static/fonts/Roboto_500.svg
  52. BIN
      static/fonts/Roboto_500.ttf
  53. BIN
      static/fonts/Roboto_500.woff
  54. BIN
      static/fonts/Roboto_500.woff2
  55. BIN
      static/fonts/Roboto_700.eot
  56. 309
    0
      static/fonts/Roboto_700.svg
  57. BIN
      static/fonts/Roboto_700.ttf
  58. BIN
      static/fonts/Roboto_700.woff
  59. BIN
      static/fonts/Roboto_700.woff2
  60. BIN
      static/fonts/fontawesome-webfont.eot
  61. 0
    2671
      static/fonts/fontawesome-webfont.svg
  62. BIN
      static/fonts/fontawesome-webfont.ttf
  63. BIN
      static/fonts/fontawesome-webfont.woff
  64. BIN
      static/fonts/fontawesome-webfont.woff2
  65. BIN
      static/img/up-arrow-black.png
  66. 0
    94
      static/img/up-arrow-black.svg
  67. BIN
      static/img/up-arrow-white.png
  68. 0
    94
      static/img/up-arrow-white.svg
  69. 76
    5
      static/js/app.js
  70. 0
    1
      static/js/app.min.js
  71. 6
    6
      static/js/bootstrap.min.js
  72. 5
    0
      static/js/fontawesome-all.min.js
  73. 0
    4
      static/js/jquery-3.2.1.min.js
  74. 2
    0
      static/js/jquery-3.3.1.min.js

+ 23
- 20
action.php View File

@@ -23,37 +23,39 @@ dieifnotloggedin();
23 23
 
24 24
 engageRateLimit();
25 25
 
26
-require_once __DIR__ . "/lib/login.php";
27
-
28 26
 function returnToSender($msg, $arg = "") {
29 27
     global $VARS;
30 28
     if ($arg == "") {
31
-        header("Location: home.php?page=" . urlencode($VARS['source']) . "&msg=$msg");
29
+        header("Location: app.php?page=" . urlencode($VARS['source']) . "&msg=$msg");
32 30
     } else {
33
-        header("Location: home.php?page=" . urlencode($VARS['source']) . "&msg=$msg&arg=" . urlencode($arg));
31
+        header("Location: app.php?page=" . urlencode($VARS['source']) . "&msg=$msg&arg=" . urlencode($arg));
34 32
     }
35 33
     die();
36 34
 }
37 35
 
38 36
 switch ($VARS['action']) {
39 37
     case "signout":
40
-        insertAuthLog(11, $_SESSION['uid']);
38
+        Log::insert(LogType::LOGOUT, $_SESSION['uid']);
41 39
         session_destroy();
42 40
         header('Location: index.php');
43 41
         die("Logged out.");
44 42
     case "chpasswd":
45 43
         $error = [];
46
-        $result = change_password($VARS['oldpass'], $VARS['newpass'], $VARS['conpass'], $error);
47
-        if ($result === TRUE) {
48
-            returnToSender("password_updated");
49
-        }
50
-        switch (count($error)) {
51
-            case 1:
52
-                returnToSender($error[0]);
53
-            case 2:
54
-                returnToSender($error[0], $error[1]);
55
-            default:
56
-                returnToSender("generic_op_error");
44
+        $user = new User($_SESSION['uid']);
45
+        try {
46
+            $result = $user->changePassword($VARS['oldpass'], $VARS['newpass'], $VARS['conpass']);
47
+
48
+            if ($result === TRUE) {
49
+                returnToSender("password_updated");
50
+            }
51
+        } catch (PasswordMatchException $e) {
52
+            returnToSender("passwords_same");
53
+        } catch (PasswordMismatchException $e) {
54
+            returnToSender("new_password_mismatch");
55
+        } catch (IncorrectPasswordException $e) {
56
+            returnToSender("old_password_mismatch");
57
+        } catch (WeakPasswordException $e) {
58
+            returnToSender("weak_password");
57 59
         }
58 60
         break;
59 61
     case "chpin":
@@ -71,16 +73,17 @@ switch ($VARS['action']) {
71 73
         if (is_empty($VARS['secret'])) {
72 74
             returnToSender("invalid_parameters");
73 75
         }
76
+        $user = new User($_SESSION['uid']);
74 77
         $totp = new TOTP(null, $VARS['secret']);
75 78
         if (!$totp->verify($VARS["totpcode"])) {
76 79
             returnToSender("2fa_wrong_code");
77 80
         }
78
-        $database->update('accounts', ['authsecret' => $VARS['secret']], ['uid' => $_SESSION['uid']]);
79
-        insertAuthLog(9, $_SESSION['uid']);
81
+        $user->save2fa($VARS['secret']);
82
+        Log::insert(LogType::ADDED_2FA, $user);
80 83
         returnToSender("2fa_enabled");
81 84
     case "rm2fa":
82
-        $database->update('accounts', ['authsecret' => ""], ['uid' => $_SESSION['uid']]);
83
-        insertAuthLog(10, $_SESSION['uid']);
85
+        (new User($_SESSION['uid']))->save2fa("");
86
+        Log::insert(LogType::REMOVED_2FA, $_SESSION['uid']);
84 87
         returnToSender("2fa_removed");
85 88
         break;
86 89
 }

+ 111
- 152
api.php View File

@@ -12,16 +12,19 @@
12 12
  * user passwords.
13 13
  */
14 14
 require __DIR__ . '/required.php';
15
-require_once __DIR__ . '/lib/login.php';
16 15
 header("Content-Type: application/json");
17 16
 
18
-//try {
19
-$key = $VARS['key'];
20
-if ($database->has('apikeys', ['key' => $key]) !== TRUE) {
21
-    engageRateLimit();
22
-    http_response_code(403);
23
-    insertAuthLog(14, null, "Key: " . $key);
17
+
18
+if (empty($VARS['key'])) {
24 19
     die("\"403 Unauthorized\"");
20
+} else {
21
+    $key = $VARS['key'];
22
+    if ($database->has('apikeys', ['key' => $key]) !== TRUE) {
23
+        engageRateLimit();
24
+        http_response_code(403);
25
+        Log::insert(LogType::API_BAD_KEY, null, "Key: " . $key);
26
+        die("\"403 Unauthorized\"");
27
+    }
25 28
 }
26 29
 
27 30
 /**
@@ -40,29 +43,31 @@ function getCensoredKey() {
40 43
     return $resp;
41 44
 }
42 45
 
46
+if (empty($VARS['action'])) {
47
+    http_response_code(404);
48
+    die(json_encode("No action specified."));
49
+}
50
+
43 51
 switch ($VARS['action']) {
44 52
     case "ping":
45 53
         exit(json_encode(["status" => "OK"]));
46 54
         break;
47 55
     case "auth":
48
-        $errmsg = "";
49
-        if (authenticate_user($VARS['username'], $VARS['password'], $errmsg)) {
50
-            insertAuthLog(12, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
56
+        $user = User::byUsername($VARS['username']);
57
+        if ($user->checkPassword($VARS['password'])) {
58
+            Log::insert(LogType::API_AUTH_OK, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
51 59
             exit(json_encode(["status" => "OK", "msg" => $Strings->get("login successful", false)]));
52 60
         } else {
53
-            insertAuthLog(13, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
54
-            if (!is_empty($errmsg)) {
55
-                exit(json_encode(["status" => "ERROR", "msg" => $Strings->build("ldap error", ['error' => $errmsg], false)]));
56
-            }
57
-            if (user_exists($VARS['username'])) {
58
-                switch (get_account_status($VARS['username'])) {
59
-                    case "LOCKED_OR_DISABLED":
61
+            Log::insert(LogType::API_AUTH_FAILED, $user->getUID(), "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
62
+            if ($user->exists()) {
63
+                switch ($user->getStatus()->get()) {
64
+                    case AccountStatus::LOCKED_OR_DISABLED:
60 65
                         exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account locked", false)]));
61
-                    case "TERMINATED":
66
+                    case AccountStatus::TERMINATED:
62 67
                         exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account terminated", false)]));
63
-                    case "CHANGE_PASSWORD":
68
+                    case AccountStatus::CHANGE_PASSWORD:
64 69
                         exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("password expired", false)]));
65
-                    case "NORMAL":
70
+                    case AccountStatus::NORMAL:
66 71
                         break;
67 72
                     default:
68 73
                         exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account state error", false)]));
@@ -72,165 +77,132 @@ switch ($VARS['action']) {
72 77
         }
73 78
         break;
74 79
     case "userinfo":
75
-        if (!is_empty($VARS['username'])) {
76
-            if (user_exists_local($VARS['username'])) {
77
-                $data = $database->select("accounts", ["uid", "username", "realname (name)", "email", "phone" => ["phone1 (1)", "phone2 (2)"], 'pin'], ["username" => strtolower($VARS['username'])])[0];
78
-                $data['pin'] = (is_null($data['pin']) || $data['pin'] == "" ? false : true);
79
-                exit(json_encode(["status" => "OK", "data" => $data]));
80
-            } else {
81
-                exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
82
-            }
83
-        } else if (!is_empty($VARS['uid'])) {
84
-            if ($database->has('accounts', ['uid' => $VARS['uid']])) {
85
-                $data = $database->select("accounts", ["uid", "username", "realname (name)", "email", "phone" => ["phone1 (1)", "phone2 (2)"], 'pin'], ["uid" => $VARS['uid']])[0];
86
-                $data['pin'] = (is_null($data['pin']) || $data['pin'] == "" ? false : true);
87
-                exit(json_encode(["status" => "OK", "data" => $data]));
88
-            } else {
89
-                exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
90
-            }
80
+        if (!empty($VARS['username'])) {
81
+            $user = User::byUsername($VARS['username']);
82
+        } else if (!empty($VARS['uid']) && is_numeric($VARS['uid'])) {
83
+            $user = new User($VARS['uid']);
91 84
         } else {
92 85
             http_response_code(400);
93 86
             die("\"400 Bad Request\"");
94 87
         }
88
+        if ($user->exists()) {
89
+            $data = $database->get("accounts", ["uid", "username", "realname (name)", "email", "phone" => ["phone1 (1)", "phone2 (2)"], 'pin'], ["uid" => $user->getUID()]);
90
+            $data['pin'] = (is_null($data['pin']) || $data['pin'] == "" ? false : true);
91
+            exit(json_encode(["status" => "OK", "data" => $data]));
92
+        } else {
93
+            exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
94
+        }
95 95
         break;
96 96
     case "userexists":
97
-        if (!is_empty($VARS['uid'])) {
98
-            if ($database->has('accounts', ['uid' => $VARS['uid']])) {
99
-                exit(json_encode(["status" => "OK", "exists" => true]));
100
-            } else {
101
-                exit(json_encode(["status" => "OK", "exists" => false]));
102
-            }
103
-        }
104
-        if (user_exists_local($VARS['username'])) {
105
-            exit(json_encode(["status" => "OK", "exists" => true]));
97
+        if (!empty($VARS['uid']) && is_numeric($VARS['uid'])) {
98
+            $user = new User($VARS['uid']);
99
+        } else if (!empty($VARS['username'])) {
100
+            $user = User::byUsername($VARS['username']);
106 101
         } else {
107
-            exit(json_encode(["status" => "OK", "exists" => false]));
102
+            http_response_code(400);
103
+            die("\"400 Bad Request\"");
108 104
         }
105
+
106
+        exit(json_encode(["status" => "OK", "exists" => $user->exists()]));
109 107
         break;
110 108
     case "hastotp":
111
-        if (userHasTOTP($VARS['username'])) {
112
-            exit(json_encode(["status" => "OK", "otp" => true]));
113
-        } else {
114
-            exit(json_encode(["status" => "OK", "otp" => false]));
115
-        }
109
+        exit(json_encode(["status" => "OK", "otp" => User::byUsername($VARS['username'])->has2fa()]));
116 110
         break;
117 111
     case "verifytotp":
118
-        if (verifyTOTP($VARS['username'], $VARS['code'])) {
112
+        $user = User::byUsername($VARS['username']);
113
+        if ($user->check2fa($VARS['code'])) {
119 114
             exit(json_encode(["status" => "OK", "valid" => true]));
120 115
         } else {
121
-            insertAuthLog(7, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
116
+            Log::insert(LogType::API_BAD_2FA, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
122 117
             exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("2fa incorrect", false), "valid" => false]));
123 118
         }
124 119
         break;
125 120
     case "acctstatus":
126
-        exit(json_encode(["status" => "OK", "account" => get_account_status($VARS['username'])]));
121
+        exit(json_encode(["status" => "OK", "account" => User::byUsername($VARS['username'])->getStatus()->getString()]));
127 122
     case "login":
128
-        engageRateLimit();
129 123
         // simulate a login, checking account status and alerts
130
-        $errmsg = "";
131
-        if (authenticate_user($VARS['username'], $VARS['password'], $errmsg)) {
132
-            $uid = $database->select('accounts', 'uid', ['username' => strtolower($VARS['username'])])[0];
133
-            switch (get_account_status($VARS['username'])) {
124
+        engageRateLimit();
125
+        $user = User::byUsername($VARS['username']);
126
+        if ($user->checkPassword($VARS['password'])) {
127
+            switch ($user->getStatus()->getString()) {
134 128
                 case "LOCKED_OR_DISABLED":
135
-                    insertAuthLog(5, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
129
+                    Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
136 130
                     exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account locked", false)]));
137 131
                 case "TERMINATED":
138
-                    insertAuthLog(5, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
132
+                    Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
139 133
                     exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account terminated", false)]));
140 134
                 case "CHANGE_PASSWORD":
141
-                    insertAuthLog(5, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
135
+                    Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
142 136
                     exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("password expired", false)]));
143 137
                 case "NORMAL":
144
-                    insertAuthLog(4, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
138
+                    Log::insert(LogType::API_LOGIN_OK, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
145 139
                     exit(json_encode(["status" => "OK"]));
146 140
                 case "ALERT_ON_ACCESS":
147
-                    sendLoginAlertEmail($VARS['username']);
148
-                    insertAuthLog(4, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
141
+                    $user->sendAlertEmail();
142
+                    Log::insert(LogType::API_LOGIN_OK, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
149 143
                     exit(json_encode(["status" => "OK", "alert" => true]));
150 144
                 default:
151
-                    insertAuthLog(5, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
145
+                    Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
152 146
                     exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account state error", false)]));
153 147
             }
154 148
         } else {
155
-            insertAuthLog(5, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
156
-            if (!is_empty($errmsg)) {
157
-                exit(json_encode(["status" => "ERROR", "msg" => $Strings->build("ldap error", ['error' => $errmsg], false)]));
158
-            }
149
+            Log::insert(LogType::API_LOGIN_FAILED, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
159 150
             exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
160 151
         }
161 152
         break;
162 153
     case "ismanagerof":
163 154
         if ($VARS['uid'] == "1") {
164
-            if ($database->has("accounts", ['uid' => $VARS['manager']])) {
165
-                if ($database->has("accounts", ['uid' => $VARS['employee']])) {
166
-                    $managerid = $VARS['manager'];
167
-                    $employeeid = $VARS['employee'];
168
-                } else {
169
-                    exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false), "user" => $VARS['employee']]));
170
-                }
171
-            } else {
172
-                exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false), "user" => $VARS['manager']]));
173
-            }
155
+            $manager = new User($VARS['manager']);
156
+            $employee = new User($VARS['employee']);
174 157
         } else {
175
-            if (user_exists_local($VARS['manager'])) {
176
-                if (user_exists_local($VARS['employee'])) {
177
-                    $managerid = $database->select('accounts', 'uid', ['username' => strtolower($VARS['manager'])]);
178
-                    $employeeid = $database->select('accounts', 'uid', ['username' => strtolower($VARS['employee'])]);
179
-                } else {
180
-                    exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false), "user" => strtolower($VARS['employee'])]));
181
-                }
182
-            } else {
183
-                exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false), "user" => strtolower($VARS['manager'])]));
184
-            }
158
+            $manager = User::byUsername($VARS['manager']);
159
+            $employee = User::byUsername($VARS['employee']);
160
+        }
161
+        if (!$manager->exists()) {
162
+            exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false), "user" => $VARS['manager']]));
185 163
         }
186
-        if ($database->has('managers', ['AND' => ['managerid' => $managerid, 'employeeid' => $employeeid]])) {
164
+        if (!$employee->exists()) {
165
+            exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false), "user" => $VARS['employee']]));
166
+        }
167
+
168
+        if ($database->has('managers', ['AND' => ['managerid' => $manager->getUID(), 'employeeid' => $employee->getUID()]])) {
187 169
             exit(json_encode(["status" => "OK", "managerof" => true]));
188 170
         } else {
189 171
             exit(json_encode(["status" => "OK", "managerof" => false]));
190 172
         }
191 173
         break;
192 174
     case "getmanaged":
193
-        if ($VARS['uid']) {
194
-            if ($database->has("accounts", ['uid' => $VARS['uid']])) {
195
-                $managerid = $VARS['uid'];
196
-            } else {
197
-                exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
198
-            }
199
-        } else if ($VARS['username']) {
200
-            if ($database->has("accounts", ['username' => strtolower($VARS['username'])])) {
201
-                $managerid = $database->select('accounts', 'uid', ['username' => strtolower($VARS['username'])]);
202
-            } else {
203
-                exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
204
-            }
175
+        if (!empty($VARS['uid'])) {
176
+            $manager = new User($VARS['uid']);
177
+        } else if (!empty($VARS['username'])) {
178
+            $manager = User::byUsername($VARS['username']);
205 179
         } else {
206 180
             http_response_code(400);
207 181
             die("\"400 Bad Request\"");
208 182
         }
183
+        if (!$manager->exists()) {
184
+            exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
185
+        }
209 186
         if ($VARS['get'] == "username") {
210
-            $managed = $database->select('managers', ['[>]accounts' => ['employeeid' => 'uid']], 'username', ['managerid' => $managerid]);
187
+            $managed = $database->select('managers', ['[>]accounts' => ['employeeid' => 'uid']], 'username', ['managerid' => $manager->getUID()]);
211 188
         } else {
212
-            $managed = $database->select('managers', 'employeeid', ['managerid' => $managerid]);
189
+            $managed = $database->select('managers', 'employeeid', ['managerid' => $manager->getUID()]);
213 190
         }
214 191
         exit(json_encode(["status" => "OK", "employees" => $managed]));
215 192
         break;
216 193
     case "getmanagers":
217
-        if ($VARS['uid']) {
218
-            if ($database->has("accounts", ['uid' => $VARS['uid']])) {
219
-                $empid = $VARS['uid'];
220
-            } else {
221
-                exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
222
-            }
223
-        } else if ($VARS['username']) {
224
-            if ($database->has("accounts", ['username' => strtolower($VARS['username'])])) {
225
-                $empid = $database->select('accounts', 'uid', ['username' => strtolower($VARS['username'])]);
226
-            } else {
227
-                exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
228
-            }
194
+        if (!empty($VARS['uid'])) {
195
+            $emp = new User($VARS['uid']);
196
+        } else if (!empty($VARS['username'])) {
197
+            $emp = User::byUsername($VARS['username']);
229 198
         } else {
230 199
             http_response_code(400);
231 200
             die("\"400 Bad Request\"");
232 201
         }
233
-        $managers = $database->select('managers', 'managerid', ['employeeid' => $empid]);
202
+        if (!$emp->exists()) {
203
+            exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
204
+        }
205
+        $managers = $database->select('managers', 'managerid', ['employeeid' => $emp->getUID()]);
234 206
         exit(json_encode(["status" => "OK", "managers" => $managers]));
235 207
         break;
236 208
     case "usersearch":
@@ -241,29 +213,23 @@ switch ($VARS['action']) {
241 213
         exit(json_encode(["status" => "OK", "result" => $data]));
242 214
         break;
243 215
     case "permission":
244
-        if (is_empty($VARS['code'])) {
216
+        if (empty($VARS['code'])) {
245 217
             http_response_code(400);
246 218
             die("\"400 Bad Request\"");
247 219
         }
248 220
         $perm = $VARS['code'];
249
-        if ($VARS['uid']) {
250
-            if ($database->has("accounts", ['uid' => $VARS['uid']])) {
251
-                $user = $database->select('accounts', ['username'], ['uid' => $VARS['uid']])[0]['username'];
252
-            } else {
253
-                exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
254
-            }
255
-        } else if ($VARS['username']) {
256
-            if ($database->has("accounts", ['username' => strtolower($VARS['username'])])) {
257
-                $user = $VARS['username'];
258
-            } else {
259
-                exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
260
-            }
221
+        if (!empty($VARS['uid'])) {
222
+            $user = new User($VARS['uid']);
223
+        } else if (!empty($VARS['username'])) {
224
+            $user = User::byUsername($VARS['username']);
261 225
         } else {
262 226
             http_response_code(400);
263 227
             die("\"400 Bad Request\"");
264 228
         }
265
-        $hasperm = account_has_permission($user, $perm);
266
-        exit(json_encode(["status" => "OK", "has_permission" => $hasperm]));
229
+        if (!$user->exists()) {
230
+            exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
231
+        }
232
+        exit(json_encode(["status" => "OK", "has_permission" => $user->hasPermission($perm)]));
267 233
         break;
268 234
     case "mobileenabled":
269 235
         exit(json_encode(["status" => "OK", "mobile" => MOBILE_ENABLED]));
@@ -277,7 +243,7 @@ switch ($VARS['action']) {
277 243
         exit(json_encode(["status" => "OK", "valid" => $user_key_valid]));
278 244
     case "alertemail":
279 245
         engageRateLimit();
280
-        if (is_empty($VARS['username']) || !user_exists($VARS['username'])) {
246
+        if (is_empty($VARS['username']) || !User::byUsername($VARS['username'])->exists()) {
281 247
             http_response_code(400);
282 248
             die("\"400 Bad Request\"");
283 249
         }
@@ -285,7 +251,7 @@ switch ($VARS['action']) {
285 251
         if (!is_empty($VARS['appname'])) {
286 252
             $appname = $VARS['appname'];
287 253
         }
288
-        $result = sendLoginAlertEmail($VARS['username'], $appname);
254
+        $result = User::byUsername($VARS['username'])->sendAlertEmail($appname);
289 255
         if ($result === TRUE) {
290 256
             exit(json_encode(["status" => "OK"]));
291 257
         }
@@ -371,22 +337,19 @@ switch ($VARS['action']) {
371 337
             http_response_code(400);
372 338
             die("\"400 Bad Request\"");
373 339
         }
374
-        if (!is_empty($VARS['username'])) {
375
-            if (user_exists_local($VARS['username'])) {
376
-                $pin = $database->get("accounts", "pin", ["username" => strtolower($VARS['username'])]);
377
-            } else {
378
-                exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
379
-            }
380
-        } else if (!is_empty($VARS['uid'])) {
381
-            if ($database->has('accounts', ['uid' => $VARS['uid']])) {
382
-                $pin = $database->get("accounts", "pin", ["uid" => strtolower($VARS['uid'])]);
383
-            } else {
384
-                exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
385
-            }
340
+        if (!empty($VARS['username'])) {
341
+            $user = User::byUsername($VARS['username']);
342
+        } else if (!empty($VARS['uid'])) {
343
+            $user = new User($VARS['uid']);
386 344
         } else {
387 345
             http_response_code(400);
388 346
             die("\"400 Bad Request\"");
389 347
         }
348
+        if ($user->exists()) {
349
+            $pin = $database->get("accounts", "pin", ["uid" => $user->getUID()]);
350
+        } else {
351
+            exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
352
+        }
390 353
         if (is_null($pin) || $pin == "") {
391 354
             exit(json_encode(["status" => "ERROR", "pinvalid" => false, "nopinset" => true]));
392 355
         }
@@ -395,8 +358,4 @@ switch ($VARS['action']) {
395 358
     default:
396 359
         http_response_code(404);
397 360
         die(json_encode("404 Not Found: the requested action is not available."));
398
-}
399
-    /* } catch (Exception $e) {
400
-      header("HTTP/1.1 500 Internal Server Error");
401
-      die("\"500 Internal Server Error\"");
402
-      } */
361
+}

+ 197
- 0
app.php View File

@@ -0,0 +1,197 @@
1
+<?php
2
+
3
+/* This Source Code Form is subject to the terms of the Mozilla Public
4
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
5
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
+
7
+require_once __DIR__ . "/required.php";
8
+
9
+if ($_SESSION['loggedin'] != true) {
10
+    header('Location: index.php');
11
+    die("Session expired.  Log in again to continue.");
12
+}
13
+
14
+require_once __DIR__ . "/pages.php";
15
+
16
+$pageid = "home";
17
+if (isset($_GET['page']) && !is_empty($_GET['page'])) {
18
+    $pg = strtolower($_GET['page']);
19
+    $pg = preg_replace('/[^0-9a-z_]/', "", $pg);
20
+    if (array_key_exists($pg, PAGES) && file_exists(__DIR__ . "/pages/" . $pg . ".php")) {
21
+        $pageid = $pg;
22
+    } else {
23
+        $pageid = "404";
24
+    }
25
+}
26
+
27
+header("Link: <static/fonts/Roboto.css>; rel=preload; as=style", false);
28
+header("Link: <static/css/bootstrap.min.css>; rel=preload; as=style", false);
29
+header("Link: <static/css/material-color/material-color.min.css>; rel=preload; as=style", false);
30
+header("Link: <static/css/app.css>; rel=preload; as=style", false);
31
+header("Link: <static/css/fa-svg-with-js.css>; rel=preload; as=style", false);
32
+header("Link: <static/js/fontawesome-all.min.js>; rel=preload; as=script", false);
33
+header("Link: <static/js/jquery-3.3.1.min.js>; rel=preload; as=script", false);
34
+header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
35
+?>
36
+<!DOCTYPE html>
37
+<html>
38
+    <head>
39
+        <meta charset="UTF-8">
40
+        <meta http-equiv="X-UA-Compatible" content="IE=edge">
41
+        <meta name="viewport" content="width=device-width, initial-scale=1">
42
+
43
+        <title><?php echo SITE_TITLE; ?></title>
44
+
45
+        <link rel="icon" href="static/img/logo.svg">
46
+
47
+        <link href="static/css/bootstrap.min.css" rel="stylesheet">
48
+        <link href="static/css/material-color/material-color.min.css" rel="stylesheet">
49
+        <link href="static/css/app.css" rel="stylesheet">
50
+        <link href="static/css/fa-svg-with-js.css" rel="stylesheet">
51
+        <script nonce="<?php echo $SECURE_NONCE; ?>">
52
+            FontAwesomeConfig = {autoAddCss: false}
53
+        </script>
54
+        <script src="static/js/fontawesome-all.min.js"></script>
55
+        <?php
56
+        // custom page styles
57
+        if (isset(PAGES[$pageid]['styles'])) {
58
+            foreach (PAGES[$pageid]['styles'] as $style) {
59
+                echo "<link href=\"$style\" rel=\"stylesheet\">\n";
60
+                header("Link: <$style>; rel=preload; as=style", false);
61
+            }
62
+        }
63
+        ?>
64
+    </head>
65
+    <body>
66
+
67
+        <?php
68
+// Alert messages
69
+        if (isset($_GET['msg']) && !is_empty($_GET['msg']) && array_key_exists($_GET['msg'], MESSAGES)) {
70
+            // optional string generation argument
71
+            if (!isset($_GET['arg']) || is_empty($_GET['arg'])) {
72
+                $alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false);
73
+            } else {
74
+                $alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
75
+            }
76
+            $alerttype = MESSAGES[$_GET['msg']]['type'];
77
+            $alerticon = "square-o";
78
+            switch (MESSAGES[$_GET['msg']]['type']) {
79
+                case "danger":
80
+                    $alerticon = "times";
81
+                    break;
82
+                case "warning":
83
+                    $alerticon = "exclamation-triangle";
84
+                    break;
85
+                case "info":
86
+                    $alerticon = "info-circle";
87
+                    break;
88
+                case "success":
89
+                    $alerticon = "check";
90
+                    break;
91
+            }
92
+            echo <<<END
93
+            <div class="row justify-content-center" id="msg-alert-box">
94
+                <div class="col-11 col-sm-6 col-md-5 col-lg-4 col-xl-4">
95
+                    <div class="alert alert-dismissible alert-$alerttype mt-2 p-0 border-0 shadow">
96
+                        <div class="p-2 pl-3">
97
+                            <button type="button" class="close">&times;</button>
98
+                            <i class="fas fa-$alerticon"></i> $alertmsg
99
+                        </div>
100
+                        <div class="progress">
101
+                            <div class="progress-bar bg-$alerttype w-0" id="msg-alert-timeout-bar"></div>
102
+                        </div>
103
+                    </div>
104
+                </div>
105
+            </div>
106
+END;
107
+        }
108
+        ?>
109
+
110
+        <?php
111
+        // Adjust as needed
112
+        $navbar_breakpoint = "md";
113
+
114
+        // For mobile app
115
+        echo "<script nonce=\"$SECURE_NONCE\">var navbar_breakpoint = \"$navbar_breakpoint\";</script>"
116
+        ?>
117
+        <nav class="navbar navbar-expand-<?php echo $navbar_breakpoint; ?> navbar-light bg-orange fixed-top">
118
+            <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">
119
+                <!--<i class="fas fa-bars"></i>-->
120
+                <span class="navbar-toggler-icon"></span>
121
+            </button>
122
+            <a class="navbar-brand py-0 mr-auto" href="app.php">
123
+                <img src="static/img/logo.svg" alt="" class="d-none d-<?php echo $navbar_breakpoint; ?>-inline brand-img py-0" />
124
+                <?php echo SITE_TITLE; ?>
125
+            </a>
126
+
127
+            <div class="collapse navbar-collapse py-0" id="navbar-collapse">
128
+                <div class="navbar-nav mr-auto py-0">
129
+                    <?php
130
+                    $curpagefound = false;
131
+                    foreach (PAGES as $id => $pg) {
132
+                        if (isset($pg['navbar']) && $pg['navbar'] === TRUE) {
133
+                            if ($pageid == $id) {
134
+                                $curpagefound = true;
135
+                                ?>
136
+                                <span class="nav-item py-<?php echo $navbar_breakpoint; ?>-0 active">
137
+                                    <?php
138
+                                } else {
139
+                                    ?>
140
+                                    <span class="nav-item py-<?php echo $navbar_breakpoint; ?>-0">
141
+                                        <?php
142
+                                    }
143
+                                    ?>
144
+                                    <a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="app.php?page=<?php echo $id; ?>">
145
+                                        <?php
146
+                                        if (isset($pg['icon'])) {
147
+                                            ?><i class="<?php echo $pg['icon']; ?> fa-fw"></i> <?php
148
+                                        }
149
+                                        $Strings->get($pg['title']);
150
+                                        ?>
151
+                                    </a>
152
+                                </span>
153
+                                <?php
154
+                            }
155
+                        }
156
+                        ?>
157
+                </div>
158
+                <div class="navbar-nav ml-auto py-0" id="navbar-right">
159
+                    <span class="nav-item py-<?php echo $navbar_breakpoint; ?>-0">
160
+                        <a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="app.php">
161
+                            <i class="fas fa-user fa-fw"></i><span>&nbsp;<?php echo $_SESSION['realname'] ?></span>
162
+                        </a>
163
+                    </span>
164
+                    <span class="nav-item mr-auto py-<?php echo $navbar_breakpoint; ?>-0">
165
+                        <a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="action.php?action=signout">
166
+                            <i class="fas fa-sign-out-alt fa-fw"></i><span>&nbsp;<?php $Strings->get("sign out") ?></span>
167
+                        </a>
168
+                    </span>
169
+                </div>
170
+            </div>
171
+        </nav>
172
+
173
+        <div class="container" id="main-content">
174
+            <div>
175
+                <?php
176
+                include_once __DIR__ . '/pages/' . $pageid . ".php";
177
+                ?>
178
+            </div>
179
+            <div class="footer">
180
+                <?php echo FOOTER_TEXT; ?><br />
181
+                Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
182
+            </div>
183
+        </div>
184
+        <script src="static/js/jquery-3.3.1.min.js"></script>
185
+        <script src="static/js/bootstrap.min.js"></script>
186
+        <script src="static/js/app.js"></script>
187
+        <?php
188
+// custom page scripts
189
+        if (isset(PAGES[$pageid]['scripts'])) {
190
+            foreach (PAGES[$pageid]['scripts'] as $script) {
191
+                echo "<script src=\"$script\"></script>\n";
192
+                header("Link: <$script>; rel=preload; as=script", false);
193
+            }
194
+        }
195
+        ?>
196
+    </body>
197
+</html>

+ 0
- 13
apps/404_error.php View File

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

+ 0
- 26
apps/change_password.php View File

@@ -1,26 +0,0 @@
1
-<?php
2
-
3
-/* This Source Code Form is subject to the terms of the Mozilla Public
4
- * License, v. 2.0. If a copy of the MPL was not distributed with this
5
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
-
7
-dieifnotloggedin();
8
-
9
-$oldpass = $Strings->get("current password", false);
10
-$newpass = $Strings->get("new password", false);
11
-$conpass = $Strings->get("confirm password", false);
12
-$change = $Strings->get("change password", false);
13
-
14
-$APPS["change_password"]["title"] = $Strings->get("change password", false);
15
-$APPS["change_password"]["icon"] = "key";
16
-$APPS["change_password"]["content"] = <<<CONTENTEND
17
-<form action="action.php" method="POST">
18
-    <input type="password" class="form-control" name="oldpass" placeholder="$oldpass" />
19
-    <input type="password" class="form-control" name="newpass" placeholder="$newpass" />
20
-    <input type="password" class="form-control" name="conpass" placeholder="$conpass" />
21
-    <input type="hidden" name="action" value="chpasswd" />
22
-    <input type="hidden" name="source" value="security" />
23
-    <br />
24
-    <button type="submit" class="btn btn-success btn-sm btn-block">$change</button>
25
-</form>
26
-CONTENTEND;

+ 0
- 27
apps/change_pin.php View File

@@ -1,27 +0,0 @@
1
-<?php
2
-
3
-/* This Source Code Form is subject to the terms of the Mozilla Public
4
- * License, v. 2.0. If a copy of the MPL was not distributed with this
5
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
-
7
-dieifnotloggedin();
8
-
9
-$newpin = $Strings->get("new pin", false);
10
-$conpin = $Strings->get("confirm pin", false);
11
-$change = $Strings->get("change pin", false);
12
-$pinexp = $Strings->get("pin explanation", false);
13
-
14
-
15
-$APPS["change_pin"]["title"] = $Strings->get("change pin", false);
16
-$APPS["change_pin"]["icon"] = "th";
17
-$APPS["change_pin"]["content"] = <<<CONTENTEND
18
-<div class="alert alert-info"><i class="fa fa-info-circle"></i> $pinexp</div>
19
-<form action="action.php" method="POST">
20
-    <input type="password" class="form-control" name="newpin" placeholder="$newpin" maxlength="8" pattern="[0-9]*" inputmode="numeric" />
21
-    <input type="password" class="form-control" name="conpin" placeholder="$conpin" maxlength="8" pattern="[0-9]*" inputmode="numeric" />
22
-    <input type="hidden" name="action" value="chpin" />
23
-    <input type="hidden" name="source" value="security" />
24
-    <br />
25
-    <button type="submit" class="btn btn-success btn-sm btn-block">$change</button>
26
-</form>
27
-CONTENTEND;

+ 0
- 79
apps/setup_2fa.php View File

@@ -1,79 +0,0 @@
1
-<?php
2
-
3
-/* This Source Code Form is subject to the terms of the Mozilla Public
4
- * License, v. 2.0. If a copy of the MPL was not distributed with this
5
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
-
7
-dieifnotloggedin();
8
-
9
-use OTPHP\Factory;
10
-use Endroid\QrCode\ErrorCorrectionLevel;
11
-use Endroid\QrCode\QrCode;
12
-
13
-// extra login utils
14
-require_once __DIR__ . "/../lib/login.php";
15
-
16
-$APPS["setup_2fa"]["title"] = $Strings->get("setup 2fa", false);
17
-$APPS["setup_2fa"]["icon"] = "lock";
18
-if (userHasTOTP($_SESSION['username'])) {
19
-    $APPS["setup_2fa"]["content"] = '<div class="alert alert-info"><i class="fa fa-info-circle"></i> ' . $Strings->get("2fa active", false) . '</div>'
20
-            . '<a href="action.php?action=rm2fa&source=security" class="btn btn-warning btn-sm btn-block">'
21
-            . $Strings->get("remove 2fa", false) . '</a>';
22
-} else if ($_GET['2fa'] == "generate") {
23
-    $codeuri = newTOTP($_SESSION['username']);
24
-    $userdata = $database->select('accounts', ['email', 'authsecret', 'realname'], ['username' => $_SESSION['username']])[0];
25
-    $label = SYSTEM_NAME . ":" . is_null($userdata['email']) ? $userdata['realname'] : $userdata['email'];
26
-    $issuer = SYSTEM_NAME;
27
-    $qrCode = new QrCode($codeuri);
28
-    $qrCode->setWriterByName('svg');
29
-    $qrCode->setSize(550);
30
-    $qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH);
31
-    $qrcode = $qrCode->writeDataUri();
32
-    $totp = Factory::loadFromProvisioningUri($codeuri);
33
-    $codesecret = $totp->getSecret();
34
-    $chunk_secret = trim(chunk_split($codesecret, 4, ' '));
35
-    $lang_manualsetup = $Strings->get("manual setup", false);
36
-    $lang_secretkey = $Strings->get("secret key", false);
37
-    $lang_label = $Strings->get("label", false);
38
-    $lang_issuer = $Strings->get("issuer", false);
39
-    $lang_entercode = $Strings->get("enter otp code", false);
40
-    $APPS["setup_2fa"]["content"] = '<div class="alert alert-info"><i class="fa fa-info-circle"></i> ' . $Strings->get("scan 2fa qrcode", false) . '</div>' . <<<END
41
-<style nonce="$SECURE_NONCE">
42
-.margintop-15px {
43
-    margin-top: 15px;
44
-}
45
-.mono-chunk {
46
-    text-align: center;
47
-    font-size: 110%;
48
-    font-family: monospace;
49
-}
50
-</style>
51
-<img src="$qrcode" class="img-responsive qrcode" />
52
-<form action="action.php" method="POST" class="margintop-15px">
53
-    <input type="text" name="totpcode" class="form-control" placeholder="$lang_entercode" minlength=6 maxlength=6 required />
54
-    <br />
55
-    <input type="hidden" name="action" value="add2fa" />
56
-    <input type="hidden" name="source" value="security" />
57
-    <input type="hidden" name="secret" value="$codesecret" />
58
-    <button type="submit" class="btn btn-success btn-sm btn-block">
59
-END
60
-            . $Strings->get("confirm 2fa", false) . <<<END
61
-    </button>
62
-</form>
63
-<div class="panel panel-default margintop-15px">
64
-    <div class="panel-body">
65
-        <b>$lang_manualsetup</b>
66
-        <br /><label>$lang_secretkey:</label>
67
-        <div class="well well-sm mono-chunk">$chunk_secret</div>
68
-        <br /><label>$lang_label:</label>
69
-        <div class="well well-sm mono-chunk">$label</div>
70
-        <br /><label>$lang_issuer:</label>
71
-        <div class="well well-sm mono-chunk">$issuer</div>
72
-    </div>
73
-</div>
74
-END;
75
-} else {
76
-    $APPS["setup_2fa"]["content"] = '<div class="alert alert-info"><i class="fa fa-info-circle"></i> ' . $Strings->get("2fa explained", false) . '</div>'
77
-            . '<a class="btn btn-success btn-sm btn-block" href="home.php?page=security&2fa=generate">'
78
-            . $Strings->get("enable 2fa", false) . '</a>';
79
-}

BIN
database.mwb View File


database_upgrade/1.0.1_1.1.sql → database_upgrade/1.0.1_2.0.sql View File


+ 1
- 269
home.php View File

@@ -3,272 +3,4 @@
3 3
  * License, v. 2.0. If a copy of the MPL was not distributed with this
4 4
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 5
 
6
-require_once __DIR__ . "/required.php";
7
-
8
-if ($_SESSION['loggedin'] != true) {
9
-    header('Location: index.php');
10
-    die("Session expired.  Log in again to continue.");
11
-} else if (is_empty($_SESSION['password'])) {
12
-    header('Location: index.php');
13
-    die("You need to log in again.");
14
-}
15
-
16
-require_once __DIR__ . "/pages.php";
17
-
18
-$pageid = "home";
19
-if (!is_empty($_GET['page'])) {
20
-    if (array_key_exists($_GET['page'], PAGES)) {
21
-        $pageid = $_GET['page'];
22
-    } else {
23
-        $pageid = "404";
24
-    }
25
-}
26
-?>
27
-<!DOCTYPE html>
28
-<html>
29
-    <head>
30
-        <meta charset="UTF-8">
31
-        <meta http-equiv="X-UA-Compatible" content="IE=edge">
32
-        <meta name="viewport" content="width=device-width, initial-scale=1">
33
-
34
-        <title><?php echo SITE_TITLE; ?></title>
35
-
36
-        <link href="static/css/bootstrap.min.css" rel="stylesheet">
37
-        <link href="static/css/font-awesome.min.css" rel="stylesheet">
38
-        <link href="static/css/material-color/material-color.min.css" rel="stylesheet">
39
-        <link href="static/css/app.css" rel="stylesheet">
40
-    </head>
41
-    <body>
42
-        <div class="container">
43
-            <div class="row">
44
-                <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">
45
-                    <?php
46
-                    if ((SHOW_ICON == "both" || SHOW_ICON == "app") && ICON_POSITION != "menu") {
47
-                        if (MENU_BAR_STYLE != "fixed") {
48
-                            ?>
49
-                            <img class="img-responsive banner-image" src="static/img/logo.svg" />
50
-                            <?php
51
-                        }
52
-                    }
53
-                    ?>
54
-                </div>
55
-            </div>
56
-            <nav class="navbar navbar-default navbar-orange navbar-<?php echo MENU_BAR_STYLE; ?>-top">
57
-                <div class="container-fluid">
58
-                    <div class="navbar-header">
59
-                        <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse">
60
-                            <span class="sr-only">Toggle navigation</span>
61
-                            <span class="icon-bar"></span>
62
-                            <span class="icon-bar"></span>
63
-                            <span class="icon-bar"></span>
64
-                        </button>
65
-                        <?php
66
-                        if (SHOW_ICON == "both" || SHOW_ICON == "app") {
67
-                            if (MENU_BAR_STYLE == "fixed" || ICON_POSITION == "menu") {
68
-                                $src = "static/img/logo.svg";
69
-                                if ($pageid != "home") {
70
-                                    $src = "static/img/up-arrow-black.png";
71
-                                }
72
-                                ?>
73
-                                <a class="navbar-brand" href="home.php">
74
-                                    <img src="<?php echo $src; ?>" />
75
-                                </a>
76
-                                <?php
77
-                            }
78
-                        }
79
-                        ?>
80
-                        <a class="navbar-brand" href="home.php">
81
-                            <?php
82
-                            echo SITE_TITLE;
83
-                            ?>
84
-                        </a>
85
-                    </div>
86
-
87
-                    <div class="collapse navbar-collapse" id="navbar-collapse">
88
-                        <ul class="nav navbar-nav">
89
-                            <?php
90
-                            $counter = 0;
91
-                            $more = "";
92
-                            $curpagefound = false;
93
-                            foreach (PAGES as $id => $pg) {
94
-                                if ($pg['navbar'] === TRUE) {
95
-                                    $counter++;
96
-                                    if ($counter > ($curpagefound ? 4 : 3) && $pageid != $id) {
97
-                                        $item = '<a href="home.php?page=' . $id . '">';
98
-                                        if (isset($pg['icon'])) {
99
-                                            $item .= '<i class="fa fa-' . $pg['icon'] . ' fa-fw"></i>';
100
-                                        }
101
-                                        $item .= $Strings->get($pg['title'], false) . '</a>';
102
-                                        echo '<li class="hidden-sm hidden-md">' . $item . "</li>";
103
-                                        $more .= '<li>' . $item . "</li>";
104
-                                    } else {
105
-                                        if ($pageid == $id) {
106
-                                            $curpagefound = true;
107
-                                            ?>
108
-                                            <li class="active">
109
-                                                <?php
110
-                                            } else {
111
-                                                ?>
112
-                                            <li>
113
-                                            <?php } ?>
114
-                                            <a href="home.php?page=<?php echo $id; ?>">
115
-                                                <?php
116
-                                                if (isset($pg['icon'])) {
117
-                                                    ?>
118
-                                                    <i class="fa fa-<?php echo $pg['icon']; ?> fa-fw"></i>
119
-                                                <?php } ?>
120
-                                                <?php $Strings->get($pg['title']) ?>
121
-                                            </a>
122
-                                        </li>
123
-                                        <?php
124
-                                    }
125
-                                }
126
-                            }
127
-
128
-                            if ($counter > 4) {
129
-                                ?>
130
-                                <li class="dropdown hidden-lg hidden-xs">
131
-                                    <a href="" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-ellipsis-v fa-fw"></i> <?php $Strings->get("more"); ?></a>
132
-                                    <ul class="dropdown-menu"><?php echo $more; ?></ul>
133
-                                </li>
134
-                            <?php } ?>
135
-                        </ul>
136
-                        <ul class="nav navbar-nav navbar-right">
137
-                            <li><a href="home.php"><i class="fa fa-user fa-fw"></i> <span class=""><?php echo $_SESSION['realname'] ?></span></a></li>
138
-                            <li><a href="action.php?action=signout"><i class="fa fa-sign-out fa-fw"></i> <span class=""><?php $Strings->get("sign out") ?></span></a></li>
139
-                        </ul>
140
-                    </div>
141
-            </nav>
142
-            <?php
143
-            if (MENU_BAR_STYLE == "fixed") {
144
-                ?>
145
-                <div class="pad-75px"></div>
146
-                <?php
147
-            }
148
-            ?>
149
-
150
-            <div class="app-dock-container mobile-app-hide">
151
-                <div class="app-dock">
152
-                    <?php
153
-                    foreach (EXTERNAL_APPS as $a) {
154
-                        ?>
155
-                        <div class="app-dock-item">
156
-                            <p>
157
-                                <a href="<?php echo $a['url']; ?>">
158
-                                    <img class="img-responsive app-icon" src="<?php
159
-                    if (strpos($a['icon'], "http") !== 0) {
160
-                        echo $a['url'] . $a['icon'];
161
-                    } else {
162
-                        echo $a['icon'];
163
-                    }
164
-                        ?>"/>
165
-                                    <span><?php echo $a['title']; ?></span>
166
-                                </a>
167
-                            </p>
168
-                        </div>
169
-                        <?php
170
-                    }
171
-                    ?>
172
-                </div>
173
-            </div>
174
-
175
-            <?php
176
-            // Alert messages
177
-            if (!is_empty($_GET['msg']) && array_key_exists($_GET['msg'], MESSAGES)) {
178
-                // optional string generation argument
179
-                if (is_empty($_GET['arg'])) {
180
-                    $alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false);
181
-                } else {
182
-                    $alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => $_GET['arg']], false);
183
-                }
184
-                $alerttype = MESSAGES[$_GET['msg']]['type'];
185
-                $alerticon = "square-o";
186
-                switch (MESSAGES[$_GET['msg']]['type']) {
187
-                    case "danger":
188
-                        $alerticon = "times";
189
-                        break;
190
-                    case "warning":
191
-                        $alerticon = "exclamation-triangle";
192
-                        break;
193
-                    case "info":
194
-                        $alerticon = "info-circle";
195
-                        break;
196
-                    case "success":
197
-                        $alerticon = "check";
198
-                        break;
199
-                }
200
-                echo <<<END
201
-            <div class="row">
202
-                <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">
203
-                    <div class="alert alert-dismissible alert-$alerttype">
204
-                        <button type="button" class="close">&times;</button>
205
-                        <i class="fa fa-$alerticon"></i> $alertmsg
206
-                    </div>
207
-                </div>
208
-            </div>
209
-END;
210
-            }
211
-            ?>
212
-            <div class="row widget-box">
213
-                <?php
214
-                // Center the widgets horizontally on the screen
215
-                $appcount = 0;
216
-                foreach (APPS[$pageid] as $app) {
217
-                    if (file_exists(__DIR__ . "/apps/" . $app . ".php")) {
218
-                        include_once __DIR__ . "/apps/" . $app . ".php";
219
-                        if (isset($APPS[$app])) {
220
-                            $appcount++;
221
-                        }
222
-                    }
223
-                }
224
-                if ($appcount == 1) {
225
-                    ?>
226
-                    <div class="hidden-xs col-sm-3 col-md-4 col-lg-4">
227
-                        <!-- Empty placeholder column for nice center-align -->
228
-                    </div>
229
-                    <?php
230
-                } else if ($appcount == 2) {
231
-                    ?>
232
-                    <div class="hidden-xs hidden-sm col-md-2 col-lg-2">
233
-                        <!-- Empty placeholder column for nice center-align -->
234
-                    </div>
235
-                    <?php
236
-                }
237
-
238
-                // Load app widgets
239
-                foreach (APPS[$pageid] as $app) {
240
-                    if (file_exists(__DIR__ . "/apps/" . $app . ".php")) {
241
-                        include_once __DIR__ . "/apps/" . $app . ".php";
242
-                        if (!isset($APPS[$app])) {
243
-                            continue;
244
-                        }
245
-                        $apptitle = ($APPS[$app]['i18n'] === TRUE ? $Strings->get($APPS[$app]['title'], false) : $APPS[$app]['title']);
246
-                        $appicon = (is_empty($APPS[$app]['icon']) ? "" : "fa fa-fw fa-" . $APPS[$app]['icon']);
247
-                        $apptype = (is_empty($APPS[$app]['type']) ? "default" : $APPS[$app]['type']);
248
-                        $appcontent = $APPS[$app]['content'];
249
-                        echo <<<END
250
-                        <div class="col-xs-12 col-sm-6 col-md-4 col-lg-4">
251
-                            <div class="panel panel-$apptype apppanel">
252
-                                <div class="panel-heading">
253
-                                    <h3 class="panel-title"><i class="$appicon"></i> $apptitle </h3>
254
-                                </div>
255
-                                <div class="panel-body">
256
-                                    $appcontent
257
-                                </div>
258
-                            </div>
259
-                        </div>
260
-END;
261
-                    }
262
-                }
263
-                ?>
264
-            </div>
265
-            <div class="footer">
266
-                <?php echo FOOTER_TEXT; ?><br />
267
-                Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
268
-            </div>
269
-        </div>
270
-        <script src="static/js/jquery-3.2.1.min.js"></script>
271
-        <script src="static/js/bootstrap.min.js"></script>
272
-        <script src="static/js/app.js"></script>
273
-    </body>
274
-</html>
6
+header("Location: app.php");

+ 133
- 138
index.php View File

@@ -5,28 +5,26 @@
5 5
 
6 6
 require_once __DIR__ . "/required.php";
7 7
 
8
-require_once __DIR__ . "/lib/login.php";
9
-
10 8
 // If we're logged in, we don't need to be here.
11
-if ($_SESSION['loggedin'] && !is_empty($_SESSION['password'])) {
9
+if (!empty($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
12 10
     header('Location: home.php');
13 11
     die();
14 12
 // This branch will likely run if the user signed in from a different app.
15
-} else if ($_SESSION['loggedin'] && is_empty($_SESSION['password'])) {
16
-    $alert = $Strings->get("sign in again", false);
17
-    $alerttype = "info";
18 13
 }
19 14
 
20 15
 /* Authenticate user */
21 16
 $username_ok = false;
22 17
 $multiauth = false;
23 18
 $change_password = false;
24
-if ($VARS['progress'] == "1") {
19
+if (empty($VARS['progress'])) {
20
+    // Easy way to remove "undefined" warnings.
21
+} else if ($VARS['progress'] == "1") {
25 22
     engageRateLimit();
26
-    if (!CAPTCHA_ENABLED || (CAPTCHA_ENABLED && verifyCaptcheck($VARS['captcheck_session_code'], $VARS['captcheck_selected_answer'], CAPTCHA_SERVER . "/api.php"))) {
23
+    if (!CAPTCHA_ENABLED || (CAPTCHA_ENABLED && Login::verifyCaptcha($VARS['captcheck_session_code'], $VARS['captcheck_selected_answer'], CAPTCHA_SERVER . "/api.php"))) {
27 24
         $autherror = "";
28
-        if (user_exists($VARS['username'])) {
29
-            $status = get_account_status($VARS['username'], $error);
25
+        $user = User::byUsername($VARS['username']);
26
+        if ($user->exists()) {
27
+            $status = $user->getStatus()->getString();
30 28
             switch ($status) {
31 29
                 case "LOCKED_OR_DISABLED":
32 30
                     $alert = $Strings->get("account locked", false);
@@ -37,15 +35,15 @@ if ($VARS['progress'] == "1") {
37 35
                 case "CHANGE_PASSWORD":
38 36
                     $alert = $Strings->get("password expired", false);
39 37
                     $alerttype = "info";
40
-                    $_SESSION['username'] = strtolower($VARS['username']);
41
-                    $_SESSION['uid'] = $database->get('accounts', 'uid', ['username' => strtolower($VARS['username'])]);
38
+                    $_SESSION['username'] = $user->getUsername();
39
+                    $_SESSION['uid'] = $user->getUID();
42 40
                     $change_password = true;
43 41
                     break;
44 42
                 case "NORMAL":
45 43
                     $username_ok = true;
46 44
                     break;
47 45
                 case "ALERT_ON_ACCESS":
48
-                    $mail_resp = sendLoginAlertEmail($VARS['username']);
46
+                    $mail_resp = $user->sendAlertEmail();
49 47
                     if (DEBUG) {
50 48
                         var_dump($mail_resp);
51 49
                     }
@@ -60,73 +58,69 @@ if ($VARS['progress'] == "1") {
60 58
                     break;
61 59
             }
62 60
             if ($username_ok) {
63
-                if (authenticate_user($VARS['username'], $VARS['password'], $autherror)) {
61
+                if ($user->checkPassword($VARS['password'])) {
64 62
                     $_SESSION['passok'] = true; // stop logins using only username and authcode
65
-                    if (userHasTOTP($VARS['username'])) {
63
+                    if ($user->has2fa()) {
66 64
                         $multiauth = true;
67
-                        $_SESSION['password'] = $VARS['password'];
68 65
                     } else {
69
-                        doLoginUser($VARS['username'], $VARS['password']);
70
-                        insertAuthLog(1, $_SESSION['uid']);
71
-                        header('Location: home.php');
72
-                        die("Logged in, go to home.php");
66
+                        Session::start($user);
67
+                        Log::insert(LogType::LOGIN_OK, $user->getUID());
68
+                        header('Location: app.php');
69
+                        die("Logged in, go to app.php");
73 70
                     }
74 71
                 } else {
75
-                    if (!is_empty($autherror)) {
76
-                        $alert = $autherror;
77
-                        insertAuthLog(2, null, "Username: " . $VARS['username']);
78
-                    } else {
79
-                        $alert = $Strings->get("login incorrect", false);
80
-                        insertAuthLog(2, null, "Username: " . $VARS['username']);
81
-                    }
72
+                    $alert = $Strings->get("login incorrect", false);
73
+                    Log::insert(LogType::LOGIN_FAILED, null, "Username: " . $VARS['username']);
82 74
                 }
83 75
             }
84 76
         } else { // User does not exist anywhere
85 77
             $alert = $Strings->get("login incorrect", false);
86
-            insertAuthLog(2, null, "Username: " . $VARS['username']);
78
+            Log::insert(LogType::LOGIN_FAILED, null, "Username: " . $VARS['username']);
87 79
         }
88 80
     } else {
89 81
         $alert = $Strings->get("captcha error", false);
90
-        insertAuthLog(8, null, "Username: " . $VARS['username']);
82
+        Log::insert(LogType::BAD_CAPTCHA, null, "Username: " . $VARS['username']);
91 83
     }
92 84
 } else if ($VARS['progress'] == "2") {
93 85
     engageRateLimit();
86
+    $user = User::byUsername($VARS['username']);
94 87
     if ($_SESSION['passok'] !== true) {
95 88
         // stop logins using only username and authcode
96 89
         sendError("Password integrity check failed!");
97 90
     }
98
-    if (verifyTOTP($VARS['username'], $VARS['authcode'])) {
99
-        doLoginUser($VARS['username'], $VARS['password']);
100
-        insertAuthLog(1, $_SESSION['uid']);
101
-        header('Location: home.php');
102
-        die("Logged in, go to home.php");
91
+    if ($user->check2fa($VARS['authcode'])) {
92
+        Session::start($user);
93
+        Log::insert(LogType::LOGIN_OK, $user->getUID());
94
+        header('Location: app.php');
95
+        die("Logged in, go to app.php");
103 96
     } else {
104 97
         $alert = $Strings->get("2fa incorrect", false);
105
-        insertAuthLog(6, null, "Username: " . $VARS['username']);
98
+        Log::insert(LogType::BAD_2FA, null, "Username: " . $VARS['username']);
106 99
     }
107 100
 } else if ($VARS['progress'] == "chpasswd") {
108 101
     engageRateLimit();
109 102
     if (!is_empty($_SESSION['username'])) {
110
-        $error = [];
111
-        $result = change_password($VARS['oldpass'], $VARS['newpass'], $VARS['conpass'], $error);
112
-        if ($result === TRUE) {
113
-            $alert = $Strings->get(MESSAGES["password_updated"]["string"], false);
114
-            $alerttype = MESSAGES["password_updated"]["type"];
115
-        }
116
-        switch (count($error)) {
117
-            case 0:
118
-                break;
119
-            case 1:
120
-                $alert = $Strings->get(MESSAGES[$error[0]]["string"], false);
121
-                $alerttype = MESSAGES[$error[0]]["type"];
122
-                break;
123
-            case 2:
124
-                $alert = $Strings->build(MESSAGES[$error[0]]["string"], ["arg" => $error[1]], false);
125
-                $alerttype = MESSAGES[$error[0]]["type"];
126
-                break;
127
-            default:
128
-                $alert = $Strings->get(MESSAGES["generic_op_error"]["string"], false);
129
-                $alerttype = MESSAGES["generic_op_error"]["type"];
103
+        $user = User::byUsername($_SESSION['username']);
104
+
105
+        try {
106
+            $result = $user->changePassword($VARS['oldpass'], $VARS['newpass'], $VARS['conpass']);
107
+
108
+            if ($result === TRUE) {
109
+                $alert = $Strings->get(MESSAGES["password_updated"]["string"], false);
110
+                $alerttype = MESSAGES["password_updated"]["type"];
111
+            }
112
+        } catch (PasswordMatchException $e) {
113
+            $alert = $Strings->get(MESSAGES["passwords_same"]["string"], false);
114
+            $alerttype = "danger";
115
+        } catch (PasswordMismatchException $e) {
116
+            $alert = $Strings->get(MESSAGES["new_password_mismatch"]["string"], false);
117
+            $alerttype = "danger";
118
+        } catch (IncorrectPasswordException $e) {
119
+            $alert = $Strings->get(MESSAGES["old_password_mismatch"]["string"], false);
120
+            $alerttype = "danger";
121
+        } catch (WeakPasswordException $e) {
122
+            $alert = $Strings->get(MESSAGES["weak_password"]["string"], false);
123
+            $alerttype = "danger";
130 124
         }
131 125
     } else {
132 126
         session_destroy();
@@ -134,6 +128,13 @@ if ($VARS['progress'] == "1") {
134 128
         die();
135 129
     }
136 130
 }
131
+
132
+header("Link: <static/fonts/Roboto.css>; rel=preload; as=style", false);
133
+header("Link: <static/css/bootstrap.min.css>; rel=preload; as=style", false);
134
+header("Link: <static/css/material-color/material-color.min.css>; rel=preload; as=style", false);
135
+header("Link: <static/css/index.css>; rel=preload; as=style", false);
136
+header("Link: <static/js/jquery-3.3.1.min.js>; rel=preload; as=script", false);
137
+header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
137 138
 ?>
138 139
 <!DOCTYPE html>
139 140
 <html>
@@ -144,101 +145,95 @@ if ($VARS['progress'] == "1") {
144 145
 
145 146
         <title><?php echo SITE_TITLE; ?></title>
146 147
 
148
+        <link rel="icon" href="static/img/logo.svg">
149
+
147 150
         <link href="static/css/bootstrap.min.css" rel="stylesheet">
148
-        <link href="static/css/font-awesome.min.css" rel="stylesheet">
149 151
         <link href="static/css/material-color/material-color.min.css" rel="stylesheet">
150
-        <link href="static/css/app.css" rel="stylesheet">
152
+        <link href="static/css/index.css" rel="stylesheet">
151 153
         <?php if (CAPTCHA_ENABLED) { ?>
152 154
             <script src="<?php echo CAPTCHA_SERVER ?>/captcheck.dist.js"></script>
153
-        <?php } ?>
155
+<?php } ?>
154 156
     </head>
155 157
     <body>
156
-        <div class="container">
157
-            <div class="row">
158
-                <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">
159
-                    <div>
158
+        <div class="row justify-content-center">
159
+            <div class="col-auto">
160
+                <img class="banner-image" src="static/img/logo.svg" />
161
+            </div>
162
+        </div>
163
+        <div class="row justify-content-center">
164
+            <div class="card col-11 col-xs-11 col-sm-8 col-md-6 col-lg-4">
165
+                <div class="card-body">
166
+                    <h5 class="card-title"><?php $Strings->get("sign in"); ?></h5>
167
+                    <form action="" method="POST">
160 168
                         <?php
161
-                        if (SHOW_ICON == "both" || SHOW_ICON == "index") {
169
+                        if (!empty($alert)) {
170
+                            $alerttype = isset($alerttype) ? $alerttype : "danger";
162 171
                             ?>
163
-                            <img class="img-responsive banner-image" src="static/img/logo.svg" />
164
-                        <?php } ?>
165
-                    </div>
166
-                    <div class="panel panel-orange">
167
-                        <div class="panel-heading">
168
-                            <h3 class="panel-title"><?php $Strings->get("sign in"); ?></h3>
169
-                        </div>
170
-                        <div class="panel-body">
171
-                            <form action="" method="POST">
172
+                            <div class="alert alert-<?php echo $alerttype ?>">
172 173
                                 <?php
173
-                                if (!is_empty($alert)) {
174
-                                    $alerttype = isset($alerttype) ? $alerttype : "danger";
175
-                                    ?>
176
-                                    <div class="alert alert-<?php echo $alerttype ?>">
177
-                                        <?php
178
-                                        switch ($alerttype) {
179
-                                            case "danger":
180
-                                                $alerticon = "times";
181
-                                                break;
182
-                                            case "warning":
183
-                                                $alerticon = "exclamation-triangle";
184
-                                                break;
185
-                                            case "info":
186
-                                                $alerticon = "info-circle";
187
-                                                break;
188
-                                            case "success":
189
-                                                $alerticon = "check";
190
-                                                break;
191
-                                            default:
192
-                                                $alerticon = "square-o";
193
-                                        }
194
-                                        ?>
195
-                                        <i class="fa fa-fw fa-<?php echo $alerticon ?>"></i> <?php echo $alert ?>
196
-                                    </div>
197
-                                    <?php
198
-                                }
199
-
200
-                                if (!$multiauth && !$change_password) {
201
-                                    ?>
202
-                                    <input type="text" class="form-control" name="username" placeholder="<?php $Strings->get("username"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
203
-                                    <input type="password" class="form-control" name="password" placeholder="<?php $Strings->get("password"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
204
-                                    <?php if (CAPTCHA_ENABLED) { ?>
205
-                                        <div class="captcheck_container" data-stylenonce="<?php echo $SECURE_NONCE; ?>"></div>
206
-                                        <br />
207
-                                    <?php } ?>
208
-                                    <input type="hidden" name="progress" value="1" />
209
-                                    <?php
210
-                                } else if ($multiauth) {
211
-                                    ?>
212
-                                    <div class="alert alert-info">
213
-                                        <?php $Strings->get("2fa prompt"); ?>
214
-                                    </div>
215
-                                    <input type="text" class="form-control" name="authcode" placeholder="<?php $Strings->get("authcode"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
216
-                                    <input type="hidden" name="progress" value="2" />
217
-                                    <input type="hidden" name="username" value="<?php echo $VARS['username']; ?>" />
218
-                                    <?php
219
-                                } else if ($change_password) {
220
-                                    ?>
221
-                                    <input type="password" class="form-control" name="oldpass" placeholder="Current password" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
222
-                                    <input type="password" class="form-control" name="newpass" placeholder="New password" required="required" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
223
-                                    <input type="password" class="form-control" name="conpass" placeholder="New password (again)" required="required" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
224
-                                    <input type="hidden" name="progress" value="chpasswd" />
225
-                                    <?php
174
+                                switch ($alerttype) {
175
+                                    case "danger":
176
+                                        $alerticon = "fas fa-times";
177
+                                        break;
178
+                                    case "warning":
179
+                                        $alerticon = "fas fa-exclamation-triangle";
180
+                                        break;
181
+                                    case "info":
182
+                                        $alerticon = "fas fa-info-circle";
183
+                                        break;
184
+                                    case "success":
185
+                                        $alerticon = "fas fa-check";
186
+                                        break;
187
+                                    default:
188
+                                        $alerticon = "far fa-square";
226 189
                                 }
227 190
                                 ?>
228
-                                <button type="submit" class="btn btn-primary">
229
-                                    <?php $Strings->get("continue"); ?>
230
-                                </button>
231
-                            </form>
232
-                        </div>
233
-                    </div>
191
+                                <i class="<?php echo $alerticon ?> fa-fw"></i> <?php echo $alert ?>
192
+                            </div>
193
+                            <?php
194
+                        }
195
+
196
+                        if (!$multiauth && !$change_password) {
197
+                            ?>
198
+                            <input type="text" class="form-control" name="username" placeholder="<?php $Strings->get("username"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
199
+                            <input type="password" class="form-control" name="password" placeholder="<?php $Strings->get("password"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
200
+    <?php if (CAPTCHA_ENABLED) { ?>
201
+                                <div class="captcheck_container" data-stylenonce="<?php echo $SECURE_NONCE; ?>"></div>
202
+                                <br />
203
+                            <?php } ?>
204
+                            <input type="hidden" name="progress" value="1" />
205
+                            <?php
206
+                        } else if ($multiauth) {
207
+                            ?>
208
+                            <div class="alert alert-info">
209
+    <?php $Strings->get("2fa prompt"); ?>
210
+                            </div>
211
+                            <input type="text" class="form-control" name="authcode" placeholder="<?php $Strings->get("authcode"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
212
+                            <input type="hidden" name="progress" value="2" />
213
+                            <input type="hidden" name="username" value="<?php echo $VARS['username']; ?>" />
214
+                            <?php
215
+                        } else if ($change_password) {
216
+                            ?>
217
+                            <input type="password" class="form-control" name="oldpass" placeholder="Current password" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
218
+                            <input type="password" class="form-control" name="newpass" placeholder="New password" required="required" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
219
+                            <input type="password" class="form-control" name="conpass" placeholder="New password (again)" required="required" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
220
+                            <input type="hidden" name="progress" value="chpasswd" />
221
+                            <?php
222
+                        }
223
+                        ?>
224
+                        <button type="submit" class="btn btn-primary">
225
+<?php $Strings->get("continue"); ?>
226
+                        </button>
227
+                    </form>
234 228
                 </div>
235 229
             </div>
236
-            <div class="footer">
237
-                <?php echo FOOTER_TEXT; ?><br />
238
-                Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
239
-            </div>
240 230
         </div>
241
-        <script src="static/js/jquery-3.2.1.min.js"></script>
242
-        <script src="static/js/bootstrap.min.js"></script>
243
-    </body>
231
+        <div class="footer">
232
+<?php echo FOOTER_TEXT; ?><br />
233
+            Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
234
+        </div>
235
+    </div>
236
+    <script src="static/js/jquery-3.3.1.min.js"></script>
237
+    <script src="static/js/bootstrap.min.js"></script>
238
+</body>
244 239
 </html>

+ 0
- 110
lang/en_us.php View File

@@ -1,110 +0,0 @@
1
-<?php
2
-
3
-/* This Source Code Form is subject to the terms of the Mozilla Public
4
- * License, v. 2.0. If a copy of the MPL was not distributed with this
5
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
-
7
-$STRINGS = [
8
-    "sign in" => "Sign In",
9
-    "username" => "Username",
10
-    "password" => "Password",
11
-    "continue" => "Continue",
12
-    "authcode" => "Authentication code",
13
-    "2fa prompt" => "Enter the six-digit code from your mobile authenticator "
14
-    . "app.",
15
-    "2fa incorrect" => "Authentication code incorrect.",
16
-    "login incorrect" => "Login incorrect.",
17
-    "login successful" => "Login successful.",
18
-    "login error" => "There was a server problem.  Try again later.",
19
-    "account locked" => "This account has been disabled. Contact technical "
20
-    . "support.",
21
-    "password expired" => "You must change your password before continuing.",
22
-    "account terminated" => "Account terminated.  Access denied.",
23
-    "account state error" => "Your account state is not stable.  Log out, "
24
-    . "restart your browser, and try again.",
25
-    "password on 500 list" => "The given password is ranked number {arg} out "
26
-    . "of the 500 most common passwords.  Try a different one.",
27
-    "welcome user" => "Welcome, {user}!",
28
-    "change password" => "Change password",
29
-    "account security" => "Account security",
30
-    "security options" => "Security options",
31
-    "account options" => "Account options",
32
-    "sync" => "Sync settings",
33
-    "options" => "Options",
34
-    "sign out" => "Sign out",
35
-    "settings" => "Settings",
36
-    "account" => "Account",
37
-    "404 error" => "404 Error",
38
-    "page not found" => "Page not found.",
39
-    "current password incorrect" => "The current password is incorrect.  "
40
-    . "Try again.",
41
-    "new password mismatch" => "The new passwords did not match.  Try again.",
42
-    "weak password" => "Password does not meet requirements.",
43
-    "password updated" => "Password updated successfully.",
44
-    "current password" => "Current password",
45
-    "new password" => "New password",
46
-    "confirm password" => "New password (again)",
47
-    "setup 2fa" => "Setup 2-factor authentication",
48
-    "2fa removed" => "2-factor authentication disabled.",
49
-    "2fa enabled" => "2-factor authentication activated.",
50
-    "remove 2fa" => "Disable 2-factor authentication",
51
-    "2fa explained" => "2-factor authentication adds more security to your "
52
-    . "account. You can use the Auth Keys (key icon) feature of the Netsyms "
53
-    . "Business Mobile app, or another TOTP-enabled app (Authy, FreeOTP, etc) on your "
54
-    . "smartphone. When you have the app installed, you can enable 2-factor "
55
-    . "authentication by clicking the button below and scanning a QR code with "
56
-    . "the app. Whenever you sign in in the future, you'll need to input a "
57
-    . "six-digit code from your phone into the login page when prompted. You "
58
-    . "can disable 2-factor authentication from this page if you change your "
59
-    . "mind.",
60
-    "2fa active" => "2-factor authentication is active on your account.  To "
61
-    . "remove 2fa, reset your authentication secret, or change to a new "
62
-    . "security device, click the button below.",
63
-    "enable 2fa" => "Enable 2-factor authentication",
64
-    "scan 2fa qrcode" => "Scan the QR Code with the authenticator app, or enter"
65
-    . " the information manually.  Then type in the six-digit code the app gives you and press Finish Setup.",
66
-    "confirm 2fa" => "Finish setup",
67
-    "invalid parameters" => "Invalid request parameters.",
68
-    "ldap server error" => "The LDAP server returned an error: {arg}",
69
-    "user does not exist" => "User does not exist.",
70
-    "captcha error" => "There was a problem with the CAPTCHA (robot test).  "
71
-    . "Try again.",
72
-    "home" => "Home",
73
-    "ldap error" => "LDAP error: {error}",
74
-    "old and new passwords match" => "Your current and new passwords are the "
75
-    . "same.",
76
-    "generic op error" => "An unknown error occurred.  Try again later.",
77
-    "password complexity insufficent" => "The new password does not meet the "
78
-    . "minumum requirements defined by your system administrator.",
79
-    "error loading widget" => "There was a problem loading this app.",
80
-    "open app" => "Open App",
81
-    "sign in again" => "Please sign in again to continue.",
82
-    "login failed try on web" => "There is a problem with your account. Visit "
83
-    . "AccountHub via a web browser for more information.",
84
-    "mobile login disabled" => "Mobile login has been disabled by your system "
85
-    . "administrator.  Contact technical support for more information.",
86
-    "admin alert email subject" => "Alert: User login notification",
87
-    "admin alert email message" => "You (or another administrator) requested to"
88
-    . " be notified when user \"{username}\" logged in, an event which happened"
89
-    . " just now."
90
-    . "\r\n"
91
-    . "\r\nUsername: \t{username}"
92
-    . "\r\nApplication: \t{appname}"
93
-    . "\r\nDate/Time: \t{datetime}"
94
-    . "\r\nIP address: \t{ipaddr}"
95
-    . "\r\n"
96
-    . "\r\nThese notifications can be disabled by editing the user in "
97
-    . "ManagePanel.",
98
-    "enter otp code" => "Enter 6-digit code",
99
-    "secret key" => "Secret key",
100
-    "label" => "Label",
101
-    "issuer" => "Issuer",
102
-    "no such code or code expired" => "That code is incorrect or expired.",
103
-    "pin explanation" => "Change or set a login PIN for the Station kiosk Quick Access.  PIN codes must be between one and eight digits.",
104
-    "change pin" => "Change PIN",
105
-    "new pin" => "New PIN",
106
-    "confirm pin" => "New PIN (again)",
107
-    "pin updated" => "PIN updated.",
108
-    "new pin mismatch" => "The new PINs don't match each other.",
109
-    "invalid pin format" => "PIN codes must be numeric and between one and eight digits in length.",
110
-];

+ 3
- 1
langs/en/api.json View File

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

+ 2
- 1
langs/en/sync.json View File

@@ -8,5 +8,6 @@
8 8
     "done adding sync code": "Done adding code",
9 9
     "manual setup": "Manual Setup:",
10 10
     "sync key": "Sync key:",
11
-    "url": "URL:"
11
+    "url": "URL:",
12
+    "sync code name": "Device nickname"
12 13
 }

lang/messages.php → langs/messages.php View File


+ 35
- 0
lib/Exceptions.lib.php View File

@@ -0,0 +1,35 @@
1
+<?php
2
+
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+class IncorrectPasswordException extends Exception {
10
+    public function __construct(string $message = "Incorrect password.", int $code = 0, \Throwable $previous = null) {
11
+        parent::__construct($message, $code, $previous);
12
+    }
13
+}
14
+
15
+class WeakPasswordException extends Exception {
16
+    public function __construct(string $message = "Password is weak or compromised.", int $code = 0, \Throwable $previous = null) {
17
+        parent::__construct($message, $code, $previous);
18
+    }
19
+}
20
+
21
+class PasswordMatchException extends Exception {
22
+
23
+    public function __construct(string $message = "Old and new passwords are identical", int $code = 0, \Throwable $previous = null) {
24
+        parent::__construct($message, $code, $previous);
25
+    }
26
+
27
+}
28
+
29
+class PasswordMismatchException extends Exception {
30
+
31
+    public function __construct(string $message = "Passwords do not match", int $code = 0, \Throwable $previous = null) {
32
+        parent::__construct($message, $code, $previous);
33
+    }
34
+
35
+}

+ 72
- 0
lib/Log.lib.php View File

@@ -0,0 +1,72 @@
1
+<?php
2
+
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+class Log {
10
+
11
+    /**
12
+     *
13
+     * @global $database
14
+     * @param int/LogType $type Either an integer (as defined by the constants in class LogType) or a LogType object.
15
+     * @param int/User $user Either a UID number or a User object.
16
+     * @param string $data Extra data to include in the log, in addition to the timestamp, log type, user, and IP address.
17
+     */
18
+    public static function insert($type, $user, string $data = "") {
19
+        global $database;
20
+        // find IP address
21
+        $ip = getClientIP();
22
+        if (gettype($type) == "object" && is_a($type, "LogType")) {
23
+            $type = $type->getType();
24
+        }
25
+
26
+        if (is_a($user, "User")) {
27
+            $uid = $user->getUID();
28
+        } else if (gettype($user) == "integer") {
29
+            $uid = $user;
30
+        } else {
31
+            $uid = null;
32
+        }
33
+
34
+        $database->insert("authlog", ['logtime' => date("Y-m-d H:i:s"), 'logtype' => $type, 'uid' => $uid, 'ip' => $ip, 'otherdata' => $data]);
35
+    }
36
+
37
+}
38
+
39
+class LogType {
40
+
41
+    const LOGIN_OK = 1;
42
+    const LOGIN_FAILED = 2;
43
+    const PASSWORD_CHANGED = 3;
44
+    const API_LOGIN_OK = 4;
45
+    const API_LOGIN_FAILED = 5;
46
+    const BAD_2FA = 6;
47
+    const API_BAD_2FA = 7;
48
+    const BAD_CAPTCHA = 8;
49
+    const ADDED_2FA = 9;
50
+    const REMOVED_2FA = 10;
51
+    const LOGOUT = 11;
52
+    const API_AUTH_OK = 12;
53
+    const API_AUTH_FAILED = 13;
54
+    const API_BAD_KEY = 14;
55
+    const LOG_CLEARED = 15;
56
+    const USER_REMOVED = 16;
57
+    const USER_ADDED = 17;
58
+    const USER_EDITED = 18;
59
+    const MOBILE_LOGIN_OK = 19;
60
+    const MOBILE_LOGIN_FAILED = 20;
61
+    const MOBILE_BAD_KEY = 21;
62
+
63
+    private $type;
64
+
65
+    function __construct(int $type) {
66
+        $this->type = $type;
67
+    }
68
+
69
+    public function getType(): int {
70
+        return $type;
71
+    }
72
+}

+ 71
- 0
lib/Login.lib.php View File

@@ -0,0 +1,71 @@
1
+<?php
2
+
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+class Login {
10
+
11
+    const BAD_USERPASS = 1;
12
+    const BAD_2FA = 2;
13
+    const ACCOUNT_DISABLED = 3;
14
+    const LOGIN_OK = 4;
15
+
16
+    public static function auth(string $username, string $password, string $twofa = ""): int {
17
+        global $database;
18
+        $username = strtolower($username);
19
+
20
+        $user = User::byUsername($username);
21
+
22
+        if (!$user->exists()) {
23
+            return Login::BAD_USERPASS;
24
+        }
25
+        if (!$user->checkPassword($password)) {
26
+            return Login::BAD_USERPASS;
27
+        }
28
+
29
+        if ($user->has2fa()) {
30
+            if (!$user->check2fa($twofa)) {
31
+                return Login::BAD_2FA;
32
+            }
33
+        }
34
+
35
+        switch ($user->getStatus()->get()) {
36
+            case AccountStatus::TERMINATED:
37
+                return Login::BAD_USERPASS;
38
+            case AccountStatus::LOCKED_OR_DISABLED:
39
+                return Login::ACCOUNT_DISABLED;
40
+            case AccountStatus::NORMAL:
41
+            default:
42
+                return Login::LOGIN_OK;
43
+        }
44
+
45
+        return Login::LOGIN_OK;
46
+    }
47
+
48
+    public static function verifyCaptcha(string $session, string $answer, string $url): bool {
49
+        $data = [
50
+            'session_id' => $session,
51
+            'answer_id' => $answer,
52
+            'action' => "verify"
53
+        ];
54
+        $options = [
55
+            'http' => [
56
+                'header' => "Content-type: application/x-www-form-urlencoded\r\n",
57
+                'method' => 'POST',
58
+                'content' => http_build_query($data)
59
+            ]
60
+        ];
61
+        $context = stream_context_create($options);
62
+        $result = file_get_contents($url, false, $context);
63
+        $resp = json_decode($result, TRUE);
64
+        if (!$resp['result']) {
65
+            return false;
66
+        } else {
67
+            return true;
68
+        }
69
+    }
70
+
71
+}

+ 19
- 0
lib/Session.lib.php View File

@@ -0,0 +1,19 @@
1
+<?php
2
+
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+class Session {
10
+
11
+    public static function start(User $user) {
12
+        $_SESSION['username'] = $user->getUsername();
13
+        $_SESSION['uid'] = $user->getUID();
14
+        $_SESSION['email'] = $user->getEmail();
15
+        $_SESSION['realname'] = $user->getName();
16
+        $_SESSION['loggedin'] = true;
17
+    }
18
+
19
+}

lib/Strings.php → lib/Strings.lib.php View File

@@ -14,7 +14,7 @@ class Strings {
14 14
     private $language = "en";
15 15
     private $strings = [];
16 16
 
17
-    function __construct($language = "en") {
17
+    public function __construct($language = "en") {
18 18
         if (!preg_match("/[a-zA-Z\_\-]+/", $language)) {
19 19
             throw new Exception("Invalid language code $language");
20 20
         }
@@ -50,7 +50,7 @@ class Strings {
50 50
      * Add language strings dynamically.
51 51
      * @param array $strings ["key" => "value", ...]
52 52
      */
53
-    function addStrings(array $strings) {
53
+    public function addStrings(array $strings) {
54 54
         foreach ($strings as $key => $val) {
55 55
             $this->strings[$key] = $val;
56 56
         }
@@ -62,7 +62,7 @@ class Strings {
62 62
      * @param bool $echo True to echo the result, false to return it.  Default is true.
63 63
      * @return string
64 64
      */
65
-    function get(string $key, bool $echo = true): string {
65
+    public function get(string $key, bool $echo = true): string {
66 66
         $str = $key;
67 67
         if (array_key_exists($key, $this->strings)) {
68 68
             $str = $this->strings[$key];
@@ -85,7 +85,7 @@ class Strings {
85 85
      * @param bool $echo True to echo the result, false to return it.  Default is true.
86 86
      * @return string
87 87
      */
88
-    function build(string $key, array $replace, bool $echo = true): string {
88
+    public function build(string $key, array $replace, bool $echo = true): string {
89 89
         $str = $key;
90 90
         if (array_key_exists($key, $this->strings)) {
91 91
             $str = $this->strings[$key];
@@ -107,7 +107,7 @@ class Strings {
107 107
      * Builds and returns a JSON key:value string for the supplied array of keys.
108 108
      * @param array $keys ["key1", "key2", ...]
109 109
      */
110
-    function getJSON(array $keys): string {
110
+    public function getJSON(array $keys): string {
111 111
         $strings = [];
112 112
         foreach ($keys as $k) {
113 113
             $strings[$k] = $this->get($k, false);

+ 307
- 0
lib/User.lib.php View File

@@ -0,0 +1,307 @@
1
+<?php
2
+
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+use Base32\Base32;
10
+use OTPHP\TOTP;
11
+
12
+class User {
13
+
14
+    private $uid = null;
15
+    private $username;
16
+    private $passhash;
17
+    private $email;
18
+    private $realname;
19
+    private $authsecret;
20
+    private $has2fa = false;
21
+    private $exists = false;
22
+
23
+    public function __construct(int $uid, string $username = "") {
24
+        global $database;
25
+        if ($database->has('accounts', ['AND' => ['uid' => $uid, 'deleted' => false]])) {
26
+            $this->uid = $uid;
27
+            $user = $database->get('accounts', ['username', 'password', 'email', 'realname', 'authsecret'], ['uid' => $uid]);
28
+            $this->username = $user['username'];
29
+            $this->passhash = $user['password'];
30
+            $this->email = $user['email'];
31
+            $this->realname = $user['realname'];
32
+            $this->authsecret = $user['authsecret'];
33
+            $this->has2fa = !empty($user['authsecret']);
34
+            $this->exists = true;
35
+        } else {
36
+            $this->uid = $uid;
37
+            $this->username = $username;
38
+        }
39
+    }
40
+
41
+    public static function byUsername(string $username): User {
42
+        global $database;
43
+        $username = strtolower($username);
44
+        if ($database->has('accounts', ['AND' => ['username' => $username, 'deleted' => false]])) {
45
+            $uid = $database->get('accounts', 'uid', ['username' => $username]);
46
+            return new self($uid * 1);
47
+        }
48
+        return new self(-1, $username);
49
+    }
50
+
51
+    /**
52
+     * Add a user to the system.  /!\ Assumes input is OK /!\
53
+     * @param string $username Username, saved in lowercase.
54
+     * @param string $password Password, will be hashed before saving.
55
+     * @param string $realname User's real legal name
56
+     * @param string $email User's email address.
57
+     * @param string $phone1 Phone number #1
58
+     * @param string $phone2 Phone number #2
59
+     * @param int $type Account type
60
+     * @return int The new user's ID number in the database.
61
+     */
62
+    public static function add(string $username, string $password, string $realname, string $email = null, string $phone1 = "", string $phone2 = "", int $type = 1): int {
63
+        global $database;
64
+        $database->insert('accounts', [
65
+            'username' => strtolower($username),
66
+            'password' => (is_null($password) ? null : encryptPassword($password)),
67
+            'realname' => $realname,
68
+            'email' => $email,
69
+            'phone1' => $phone1,
70
+            'phone2' => $phone2,
71
+            'acctstatus' => 1,
72
+            'accttype' => $type
73
+        ]);
74
+        return $database->id();
75
+    }
76
+
77
+    public function exists(): bool {
78
+        return $this->exists;
79
+    }
80
+
81
+    public function has2fa(): bool {
82
+        return $this->has2fa;
83
+    }
84
+
85
+    function getUsername() {
86
+        return $this->username;
87
+    }
88
+
89
+    function getUID() {
90
+        return $this->uid;
91
+    }
92
+
93
+    function getEmail() {
94
+        return $this->email;
95
+    }
96
+
97
+    function getName() {
98
+        return $this->realname;
99
+    }
100
+
101
+    /**
102
+     * Check the given plaintext password against the stored hash.
103
+     * @param string $password
104
+     * @return bool
105
+     */
106
+    function checkPassword(string $password): bool {
107
+        return password_verify($password, $this->passhash);
108
+    }
109
+
110
+    /**
111
+     * Change the user's password.
112
+     * @global $database $database
113
+     * @param string $old The current password
114
+     * @param string $new The new password
115
+     * @param string $new2 New password again
116
+     * @throws PasswordMatchException
117
+     * @throws PasswordMismatchException
118
+     * @throws IncorrectPasswordException
119
+     * @throws WeakPasswordException
120
+     */
121
+    function changePassword(string $old, string $new, string $new2) {
122
+        global $database;
123
+        if ($old == $new) {
124
+            throw new PasswordMatchException();
125
+        }
126
+        if ($new != $new2) {
127
+            throw new PasswordMismatchException();
128
+        }
129
+
130
+        if (!$this->checkPassword($old)) {
131
+            throw new IncorrectPasswordException();
132
+        }
133
+
134
+        require_once __DIR__ . "/worst_passwords.php";
135
+
136
+        $passrank = checkWorst500List($new);
137
+        if ($passrank !== FALSE) {
138
+            throw new WeakPasswordException();
139
+        }
140
+        if (strlen($new) < MIN_PASSWORD_LENGTH) {
141
+            throw new WeakPasswordException();
142
+        }
143
+
144
+        $database->update('accounts', ['password' => password_hash($new, PASSWORD_DEFAULT), 'acctstatus' => 1], ['uid' => $this->uid]);
145
+        Log::insert(LogType::PASSWORD_CHANGED, $this);
146
+        return true;
147
+    }
148
+
149
+    function check2fa(string $code): bool {
150
+        if (!$this->has2fa) {
151
+            return true;
152
+        }
153
+
154
+        $totp = new TOTP(null, $this->authsecret);
155
+        $time = time();
156
+        if ($totp->verify($code, $time)) {
157
+            return true;
158
+        }
159
+        if ($totp->verify($code, $time - 30)) {
160
+            return true;
161
+        }
162
+        if ($totp->verify($code, $time + 30)) {
163
+            return true;
164
+        }
165
+
166
+        return false;
167
+    }
168
+
169
+    /**
170
+     * Generate a TOTP secret for the given user.
171
+     * @return string OTP provisioning URI (for generating a QR code)
172
+     */
173
+    function generate2fa(): string {
174
+        $secret = random_bytes(20);
175
+        $encoded_secret = Base32::encode($secret);
176
+        $totp = new TOTP((empty($this->email) ? $this->realname : $this->email), $encoded_secret);
177
+        $totp->setIssuer(SYSTEM_NAME);
178
+        return $totp->getProvisioningUri();
179
+    }
180
+
181
+    /**
182
+     * Save a TOTP secret for the user.
183
+     * @global $database $database
184
+     * @param string $username
185
+     * @param string $secret
186
+     */
187
+    function save2fa(string $secret) {
188
+        global $database;
189
+        $database->update('accounts', ['authsecret' => $secret], ['username' => $this->username]);
190
+    }
191
+
192
+    /**
193
+     * Check if the given username has the given permission (or admin access)
194
+     * @global $database $database
195
+     * @param string $code
196
+     * @return boolean TRUE if the user has the permission (or admin access), else FALSE
197
+     */
198
+    function hasPermission(string $code): bool {
199
+        global $database;
200
+        return $database->has('assigned_permissions', [
201
+                    '[>]permissions' => [
202
+                        'permid' => 'permid'
203
+                    ]
204
+                        ], ['AND' => ['OR' => ['permcode #code' => $code, 'permcode #admin' => 'ADMIN'], 'uid' => $this->uid]]) === TRUE;
205
+    }
206
+
207
+    /**
208
+     * Get the account status.
209
+     * @return \AccountStatus
210
+     */
211
+    function getStatus(): AccountStatus {
212
+        global $database;
213
+        $statuscode = $database->get('accounts', 'acctstatus', ['uid' => $this->uid]);
214
+        return new AccountStatus($statuscode);
215
+    }
216
+
217
+    function sendAlertEmail(string $appname = SITE_TITLE) {
218
+        if (is_empty(ADMIN_EMAIL) || filter_var(ADMIN_EMAIL, FILTER_VALIDATE_EMAIL) === FALSE) {
219
+            return "invalid_to_email";
220
+        }
221
+        if (is_empty(FROM_EMAIL) || filter_var(FROM_EMAIL, FILTER_VALIDATE_EMAIL) === FALSE) {
222
+            return "invalid_from_email";
223
+        }
224
+
225
+        $mail = new PHPMailer;
226
+
227
+        if (DEBUG) {
228
+            $mail->SMTPDebug = 2;
229
+        }
230
+
231
+        if (USE_SMTP) {
232
+            $mail->isSMTP();
233
+            $mail->Host = SMTP_HOST;
234
+            $mail->SMTPAuth = SMTP_AUTH;
235
+            $mail->Username = SMTP_USER;
236
+            $mail->Password = SMTP_PASS;
237
+            $mail->SMTPSecure = SMTP_SECURE;
238
+            $mail->Port = SMTP_PORT;
239
+            if (SMTP_ALLOW_INVALID_CERTIFICATE) {
240
+                $mail->SMTPOptions = array(
241
+                    'ssl' => array(
242
+                        'verify_peer' => false,
243
+                        'verify_peer_name' => false,
244
+                        'allow_self_signed' => true
245
+                    )
246
+                );
247
+            }
248
+        }
249
+
250
+        $mail->setFrom(FROM_EMAIL, 'Account Alerts');
251
+        $mail->addAddress(ADMIN_EMAIL, "System Admin");
252
+        $mail->isHTML(false);
253
+        $mail->Subject = $Strings->get("admin alert email subject", false);
254
+        $mail->Body = $Strings->build("admin alert email message", ["username" => $this->username, "datetime" => date("Y-m-d H:i:s"), "ipaddr" => getClientIP(), "appname" => $appname], false);
255
+
256
+        if (!$mail->send()) {
257
+            return $mail->ErrorInfo;
258
+        }
259
+        return true;
260
+    }
261
+
262
+}
263
+
264
+class AccountStatus {
265
+
266
+    const NORMAL = 1;
267
+    const LOCKED_OR_DISABLED = 2;
268
+    const CHANGE_PASSWORD = 3;
269
+    const TERMINATED = 4;
270
+    const ALERT_ON_ACCESS = 5;
271
+
272
+    private $status;
273
+
274
+    public function __construct(int $status) {
275
+        $this->status = $status;
276
+    }
277
+
278
+    /**
279
+     * Get the account status/state as an integer.
280
+     * @return int
281
+     */
282
+    public function get(): int {
283
+        return $this->status;
284
+    }
285
+
286
+    /**
287
+     * Get the account status/state as a string representation.
288
+     * @return string
289
+     */
290
+    public function getString(): string {
291
+        switch ($this->status) {
292
+            case self::NORMAL:
293
+                return "NORMAL";
294
+            case self::LOCKED_OR_DISABLED:
295
+                return "LOCKED_OR_DISABLED";
296
+            case self::CHANGE_PASSWORD:
297
+                return "CHANGE_PASSWORD";
298
+            case self::TERMINATED:
299
+                return "TERMINATED";
300
+            case self::ALERT_ON_ACCESS:
301
+                return "ALERT_ON_ACCESS";
302
+            default:
303
+                return "OTHER_" . $this->status;
304
+        }
305
+    }
306
+
307
+}

+ 0
- 538
lib/login.php View File

@@ -1,538 +0,0 @@
1
-<?php
2
-
3
-/* This Source Code Form is subject to the terms of the Mozilla Public
4
- * License, v. 2.0. If a copy of the MPL was not distributed with this
5
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
-
7
-/**
8
- * Authentication and account functions
9
- */
10
-use Base32\Base32;
11
-use OTPHP\TOTP;
12
-use LdapTools\LdapManager;
13
-use LdapTools\Object\LdapObjectType;
14
-
15
-$ldap = new LdapManager($ldap_config);
16
-
17
-////////////////////////////////////////////////////////////////////////////////
18
-//                           Account handling                                 //
19
-////////////////////////////////////////////////////////////////////////////////
20
-
21
-/**
22
- * Add a user to the system.  /!\ Assumes input is OK /!\
23
- * @param string $username Username, saved in lowercase.
24
- * @param string $password Password, will be hashed before saving.
25
- * @param string $realname User's real legal name
26
- * @param string $email User's email address.
27
- * @param string $phone1 Phone number #1
28
- * @param string $phone2 Phone number #2
29
- * @param int $type Account type
30
- * @return int The new user's ID number in the database.
31
- */
32
-function adduser($username, $password, $realname, $email = null, $phone1 = "", $phone2 = "", $type = 1) {
33
-    global $database;
34
-    $database->insert('accounts', [
35
-        'username' => strtolower($username),
36
-        'password' => (is_null($password) ? null : encryptPassword($password)),
37
-        'realname' => $realname,
38
-        'email' => $email,
39
-        'phone1' => $phone1,
40
-        'phone2' => $phone2,
41
-        'acctstatus' => 1,
42
-        'accttype' => $type
43
-    ]);
44
-    //var_dump($database->error());
45
-    return $database->id();
46
-}
47
-
48
-/**
49
- * Change the password for the current user.
50
- * @global $database $database
51
- * @global LdapManager $ldap
52
- * @param string $old The current password
53
- * @param string $new The new password
54
- * @param string $new2 New password again
55
- * @param [string] $error If the function returns false, this will have an array
56
- * with a message ID from `lang/messages.php` and (depending on the message) an
57
- * extra string for that message.
58
- * @return boolean true if the password is changed, else false
59
- */
60
-function change_password($old, $new, $new2, &$error) {
61
-    global $database, $ldap;
62
-    // make sure the new password isn't the same as the current one
63
-    if ($old == $new) {
64
-        $error = ["passwords_same"];
65
-        return false;
66
-    }
67
-    // Make sure the new passwords are the same
68
-    if ($new != $new2) {
69
-        $error = ["new_password_mismatch"];
70
-        return false;
71
-    }
72
-    // check the current password
73
-    $login_ok = authenticate_user($_SESSION['username'], $old, $errmsg, $errcode);
74
-    // Allow login if the error is due to expired password
75
-    if (!$login_ok && ($errcode == LdapTools\Connection\ADResponseCodes::ACCOUNT_PASSWORD_EXPIRED || $errcode == LdapTools\Connection\ADResponseCodes::ACCOUNT_PASSWORD_MUST_CHANGE)) {
76
-        $login_ok = true;
77
-    }
78
-    if ($login_ok) {
79
-        // Check the new password and make sure it's not stupid
80
-        require_once __DIR__ . "/worst_passwords.php";
81
-        $passrank = checkWorst500List($new);
82
-        if ($passrank !== FALSE) {
83
-            $error = ["password_500", $passrank];
84
-            return false;
85
-        }
86
-        if (strlen($new) < MIN_PASSWORD_LENGTH) {
87
-            $error = ["weak_password"];
88
-            return false;
89
-        }
90
-
91
-        // Figure out how to change the password, then do it
92
-        $acctloc = account_location($_SESSION['username']);
93
-        if ($acctloc == "LOCAL") {
94
-            $database->update('accounts', ['password' => encryptPassword($new), 'acctstatus' => 1], ['uid' => $_SESSION['uid']]);
95
-            $_SESSION['password'] = $new;
96
-            insertAuthLog(3, $_SESSION['uid']);
97
-            return true;
98
-        } else if ($acctloc == "LDAP") {
99
-            try {
100
-                $repository = $ldap->getRepository(LdapObjectType::USER);
101
-                $user = $repository->findOneByUsername($_SESSION['username']);
102
-                $user->setPassword($new);
103
-                $user->setpasswordMustChange(false);
104
-                $ldap->persist($user);
105
-                $database->update('accounts', ['acctstatus' => 1], ['uid' => $_SESSION['uid']]);
106
-                insertAuthLog(3, $_SESSION['uid']);
107
-                $_SESSION['password'] = $new;
108
-                return true;
109
-            } catch (\Exception $e) {
110
-                // Stupid password complexity BS error
111
-                if (strpos($e->getMessage(), "DSID-031A11E5") !== FALSE) {
112
-                    $error = ["password_complexity"];
113
-                    return false;
114
-                }
115
-                $error = ["ldap_error", $e->getMessage()];
116
-                return false;
117
-            }
118
-        }
119
-        $error = ["account_state_error"];
120
-        return false;
121
-    }
122
-    $error = ["old_password_mismatch"];
123
-    return false;
124
-}
125
-
126
-/**
127
- * Get where a user's account actually is.
128
- * @param string $username
129
- * @return string "LDAP", "LOCAL", "LDAP_ONLY", or "NONE".
130
- */
131
-function account_location($username) {
132
-    global $database;
133
-    $username = strtolower($username);
134
-    $user_exists_local = user_exists_local($username);
135
-    if (!$user_exists_local && !LDAP_ENABLED) {
136
-        return "NONE";
137
-    }
138
-    if ($user_exists_local) {
139
-        $userinfo = $database->select('accounts', ['password'], ['username' => $username])[0];
140
-        // if password empty, it's an LDAP user
141
-        if (!is_empty($userinfo['password'])) {
142
-            return "LOCAL";
143
-        } else if (is_empty($userinfo['password']) && LDAP_ENABLED) {
144
-            return "LDAP";
145
-        } else {
146
-            return "NONE";
147
-        }
148
-    } else {
149
-        if (user_exists_ldap($username)) {
150
-            return "LDAP_ONLY";
151
-        } else {
152
-            return "NONE";
153
-        }
154
-    }
155
-}
156
-
157
-/**
158
- * Checks the given credentials against the database.
159
- * @param string $username
160
- * @param string $password
161
- * @return boolean True if OK, else false
162
- */
163
-function authenticate_user($username, $password, &$errormsg = null, &$errorcode = null) {
164
-    global $database;
165
-    global $ldap;
166
-    $username = strtolower($username);
167
-    if (is_empty($username) || is_empty($password)) {
168
-        return "NONE";
169
-    }
170
-    $loc = account_location($username, $password);
171
-    switch ($loc) {
172
-        case "LOCAL":
173
-            $hash = $database->select('accounts', ['password'], ['username' => $username, "LIMIT" => 1])[0]['password'];
174
-            return (comparePassword($password, $hash));
175
-        case "LDAP":
176
-            return authenticate_user_ldap($username, $password, $errormsg, $errorcode) === TRUE;
177
-        case "LDAP_ONLY":
178
-            // Authenticate with LDAP and create database account
179
-            try {
180
-                if (authenticate_user_ldap($username, $password, $errormsg, $errorcode) === TRUE) {
181
-                    $user = $ldap->getRepository('user')->findOneByUsername($username);
182
-
183
-                    adduser($user->getUsername(), null, $user->getName(), ($user->hasEmailAddress() ? $user->getEmailAddress() : null), "", "", 2);
184
-                    return true;
185
-                }
186
-                return false;
187
-            } catch (Exception $e) {
188
-                $errormsg = $e->getMessage();
189
-                return false;
190
-            }
191
-        default:
192
-            return false;
193
-    }
194
-}
195
-
196
-function user_exists($username) {
197
-    return account_location(strtolower($username)) !== "NONE";
198
-}
199
-
200
-/**
201
- * Check if a username exists in the local database.
202
- * @param String $username
203
- */
204
-function user_exists_local($username) {
205
-    global $database;
206
-    $username = strtolower($username);
207
-    return $database->has('accounts', ['username' => $username]) === TRUE;
208
-}
209
-