Browse Source

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

tags/v2.0
Skylar Ittner 1 year 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();

engageRateLimit();

require_once __DIR__ . "/lib/login.php";

function returnToSender($msg, $arg = "") {
global $VARS;
if ($arg == "") {
header("Location: home.php?page=" . urlencode($VARS['source']) . "&msg=$msg");
header("Location: app.php?page=" . urlencode($VARS['source']) . "&msg=$msg");
} else {
header("Location: home.php?page=" . urlencode($VARS['source']) . "&msg=$msg&arg=" . urlencode($arg));
header("Location: app.php?page=" . urlencode($VARS['source']) . "&msg=$msg&arg=" . urlencode($arg));
}
die();
}

switch ($VARS['action']) {
case "signout":
insertAuthLog(11, $_SESSION['uid']);
Log::insert(LogType::LOGOUT, $_SESSION['uid']);
session_destroy();
header('Location: index.php');
die("Logged out.");
case "chpasswd":
$error = [];
$result = change_password($VARS['oldpass'], $VARS['newpass'], $VARS['conpass'], $error);
if ($result === TRUE) {
returnToSender("password_updated");
}
switch (count($error)) {
case 1:
returnToSender($error[0]);
case 2:
returnToSender($error[0], $error[1]);
default:
returnToSender("generic_op_error");
$user = new User($_SESSION['uid']);
try {
$result = $user->changePassword($VARS['oldpass'], $VARS['newpass'], $VARS['conpass']);

if ($result === TRUE) {
returnToSender("password_updated");
}
} catch (PasswordMatchException $e) {
returnToSender("passwords_same");
} catch (PasswordMismatchException $e) {
returnToSender("new_password_mismatch");
} catch (IncorrectPasswordException $e) {
returnToSender("old_password_mismatch");
} catch (WeakPasswordException $e) {
returnToSender("weak_password");
}
break;
case "chpin":
@@ -71,16 +73,17 @@ switch ($VARS['action']) {
if (is_empty($VARS['secret'])) {
returnToSender("invalid_parameters");
}
$user = new User($_SESSION['uid']);
$totp = new TOTP(null, $VARS['secret']);
if (!$totp->verify($VARS["totpcode"])) {
returnToSender("2fa_wrong_code");
}
$database->update('accounts', ['authsecret' => $VARS['secret']], ['uid' => $_SESSION['uid']]);
insertAuthLog(9, $_SESSION['uid']);
$user->save2fa($VARS['secret']);
Log::insert(LogType::ADDED_2FA, $user);
returnToSender("2fa_enabled");
case "rm2fa":
$database->update('accounts', ['authsecret' => ""], ['uid' => $_SESSION['uid']]);
insertAuthLog(10, $_SESSION['uid']);
(new User($_SESSION['uid']))->save2fa("");
Log::insert(LogType::REMOVED_2FA, $_SESSION['uid']);
returnToSender("2fa_removed");
break;
}

+ 111
- 152
api.php View File

@@ -12,16 +12,19 @@
* user passwords.
*/
require __DIR__ . '/required.php';
require_once __DIR__ . '/lib/login.php';
header("Content-Type: application/json");

//try {
$key = $VARS['key'];
if ($database->has('apikeys', ['key' => $key]) !== TRUE) {
engageRateLimit();
http_response_code(403);
insertAuthLog(14, null, "Key: " . $key);

if (empty($VARS['key'])) {
die("\"403 Unauthorized\"");
} else {
$key = $VARS['key'];
if ($database->has('apikeys', ['key' => $key]) !== TRUE) {
engageRateLimit();
http_response_code(403);
Log::insert(LogType::API_BAD_KEY, null, "Key: " . $key);
die("\"403 Unauthorized\"");
}
}

/**
@@ -40,29 +43,31 @@ function getCensoredKey() {
return $resp;
}

if (empty($VARS['action'])) {
http_response_code(404);
die(json_encode("No action specified."));
}

switch ($VARS['action']) {
case "ping":
exit(json_encode(["status" => "OK"]));
break;
case "auth":
$errmsg = "";
if (authenticate_user($VARS['username'], $VARS['password'], $errmsg)) {
insertAuthLog(12, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
$user = User::byUsername($VARS['username']);
if ($user->checkPassword($VARS['password'])) {
Log::insert(LogType::API_AUTH_OK, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "OK", "msg" => $Strings->get("login successful", false)]));
} else {
insertAuthLog(13, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
if (!is_empty($errmsg)) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->build("ldap error", ['error' => $errmsg], false)]));
}
if (user_exists($VARS['username'])) {
switch (get_account_status($VARS['username'])) {
case "LOCKED_OR_DISABLED":
Log::insert(LogType::API_AUTH_FAILED, $user->getUID(), "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
if ($user->exists()) {
switch ($user->getStatus()->get()) {
case AccountStatus::LOCKED_OR_DISABLED:
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account locked", false)]));
case "TERMINATED":
case AccountStatus::TERMINATED:
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account terminated", false)]));
case "CHANGE_PASSWORD":
case AccountStatus::CHANGE_PASSWORD:
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("password expired", false)]));
case "NORMAL":
case AccountStatus::NORMAL:
break;
default:
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account state error", false)]));
@@ -72,165 +77,132 @@ switch ($VARS['action']) {
}
break;
case "userinfo":
if (!is_empty($VARS['username'])) {
if (user_exists_local($VARS['username'])) {
$data = $database->select("accounts", ["uid", "username", "realname (name)", "email", "phone" => ["phone1 (1)", "phone2 (2)"], 'pin'], ["username" => strtolower($VARS['username'])])[0];
$data['pin'] = (is_null($data['pin']) || $data['pin'] == "" ? false : true);
exit(json_encode(["status" => "OK", "data" => $data]));
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
}
} else if (!is_empty($VARS['uid'])) {
if ($database->has('accounts', ['uid' => $VARS['uid']])) {
$data = $database->select("accounts", ["uid", "username", "realname (name)", "email", "phone" => ["phone1 (1)", "phone2 (2)"], 'pin'], ["uid" => $VARS['uid']])[0];
$data['pin'] = (is_null($data['pin']) || $data['pin'] == "" ? false : true);
exit(json_encode(["status" => "OK", "data" => $data]));
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
}
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid']) && is_numeric($VARS['uid'])) {
$user = new User($VARS['uid']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
if ($user->exists()) {
$data = $database->get("accounts", ["uid", "username", "realname (name)", "email", "phone" => ["phone1 (1)", "phone2 (2)"], 'pin'], ["uid" => $user->getUID()]);
$data['pin'] = (is_null($data['pin']) || $data['pin'] == "" ? false : true);
exit(json_encode(["status" => "OK", "data" => $data]));
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
}
break;
case "userexists":
if (!is_empty($VARS['uid'])) {
if ($database->has('accounts', ['uid' => $VARS['uid']])) {
exit(json_encode(["status" => "OK", "exists" => true]));
} else {
exit(json_encode(["status" => "OK", "exists" => false]));
}
}
if (user_exists_local($VARS['username'])) {
exit(json_encode(["status" => "OK", "exists" => true]));
if (!empty($VARS['uid']) && is_numeric($VARS['uid'])) {
$user = new User($VARS['uid']);
} else if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else {
exit(json_encode(["status" => "OK", "exists" => false]));
http_response_code(400);
die("\"400 Bad Request\"");
}

exit(json_encode(["status" => "OK", "exists" => $user->exists()]));
break;
case "hastotp":
if (userHasTOTP($VARS['username'])) {
exit(json_encode(["status" => "OK", "otp" => true]));
} else {
exit(json_encode(["status" => "OK", "otp" => false]));
}
exit(json_encode(["status" => "OK", "otp" => User::byUsername($VARS['username'])->has2fa()]));
break;
case "verifytotp":
if (verifyTOTP($VARS['username'], $VARS['code'])) {
$user = User::byUsername($VARS['username']);
if ($user->check2fa($VARS['code'])) {
exit(json_encode(["status" => "OK", "valid" => true]));
} else {
insertAuthLog(7, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
Log::insert(LogType::API_BAD_2FA, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("2fa incorrect", false), "valid" => false]));
}
break;
case "acctstatus":
exit(json_encode(["status" => "OK", "account" => get_account_status($VARS['username'])]));
exit(json_encode(["status" => "OK", "account" => User::byUsername($VARS['username'])->getStatus()->getString()]));
case "login":
engageRateLimit();
// simulate a login, checking account status and alerts
$errmsg = "";
if (authenticate_user($VARS['username'], $VARS['password'], $errmsg)) {
$uid = $database->select('accounts', 'uid', ['username' => strtolower($VARS['username'])])[0];
switch (get_account_status($VARS['username'])) {
engageRateLimit();
$user = User::byUsername($VARS['username']);
if ($user->checkPassword($VARS['password'])) {
switch ($user->getStatus()->getString()) {
case "LOCKED_OR_DISABLED":
insertAuthLog(5, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account locked", false)]));
case "TERMINATED":
insertAuthLog(5, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account terminated", false)]));
case "CHANGE_PASSWORD":
insertAuthLog(5, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("password expired", false)]));
case "NORMAL":
insertAuthLog(4, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
Log::insert(LogType::API_LOGIN_OK, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "OK"]));
case "ALERT_ON_ACCESS":
sendLoginAlertEmail($VARS['username']);
insertAuthLog(4, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
$user->sendAlertEmail();
Log::insert(LogType::API_LOGIN_OK, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "OK", "alert" => true]));
default:
insertAuthLog(5, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account state error", false)]));
}
} else {
insertAuthLog(5, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
if (!is_empty($errmsg)) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->build("ldap error", ['error' => $errmsg], false)]));
}
Log::insert(LogType::API_LOGIN_FAILED, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
}
break;
case "ismanagerof":
if ($VARS['uid'] == "1") {
if ($database->has("accounts", ['uid' => $VARS['manager']])) {
if ($database->has("accounts", ['uid' => $VARS['employee']])) {
$managerid = $VARS['manager'];
$employeeid = $VARS['employee'];
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false), "user" => $VARS['employee']]));
}
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false), "user" => $VARS['manager']]));
}
$manager = new User($VARS['manager']);
$employee = new User($VARS['employee']);
} else {
if (user_exists_local($VARS['manager'])) {
if (user_exists_local($VARS['employee'])) {
$managerid = $database->select('accounts', 'uid', ['username' => strtolower($VARS['manager'])]);
$employeeid = $database->select('accounts', 'uid', ['username' => strtolower($VARS['employee'])]);
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false), "user" => strtolower($VARS['employee'])]));
}
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false), "user" => strtolower($VARS['manager'])]));
}
$manager = User::byUsername($VARS['manager']);
$employee = User::byUsername($VARS['employee']);
}
if (!$manager->exists()) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false), "user" => $VARS['manager']]));
}
if ($database->has('managers', ['AND' => ['managerid' => $managerid, 'employeeid' => $employeeid]])) {
if (!$employee->exists()) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false), "user" => $VARS['employee']]));
}

if ($database->has('managers', ['AND' => ['managerid' => $manager->getUID(), 'employeeid' => $employee->getUID()]])) {
exit(json_encode(["status" => "OK", "managerof" => true]));
} else {
exit(json_encode(["status" => "OK", "managerof" => false]));
}
break;
case "getmanaged":
if ($VARS['uid']) {
if ($database->has("accounts", ['uid' => $VARS['uid']])) {
$managerid = $VARS['uid'];
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
}
} else if ($VARS['username']) {
if ($database->has("accounts", ['username' => strtolower($VARS['username'])])) {
$managerid = $database->select('accounts', 'uid', ['username' => strtolower($VARS['username'])]);
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
}
if (!empty($VARS['uid'])) {
$manager = new User($VARS['uid']);
} else if (!empty($VARS['username'])) {
$manager = User::byUsername($VARS['username']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
if (!$manager->exists()) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
}
if ($VARS['get'] == "username") {
$managed = $database->select('managers', ['[>]accounts' => ['employeeid' => 'uid']], 'username', ['managerid' => $managerid]);
$managed = $database->select('managers', ['[>]accounts' => ['employeeid' => 'uid']], 'username', ['managerid' => $manager->getUID()]);
} else {
$managed = $database->select('managers', 'employeeid', ['managerid' => $managerid]);
$managed = $database->select('managers', 'employeeid', ['managerid' => $manager->getUID()]);
}
exit(json_encode(["status" => "OK", "employees" => $managed]));
break;
case "getmanagers":
if ($VARS['uid']) {
if ($database->has("accounts", ['uid' => $VARS['uid']])) {
$empid = $VARS['uid'];
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
}
} else if ($VARS['username']) {
if ($database->has("accounts", ['username' => strtolower($VARS['username'])])) {
$empid = $database->select('accounts', 'uid', ['username' => strtolower($VARS['username'])]);
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
}
if (!empty($VARS['uid'])) {
$emp = new User($VARS['uid']);
} else if (!empty($VARS['username'])) {
$emp = User::byUsername($VARS['username']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
$managers = $database->select('managers', 'managerid', ['employeeid' => $empid]);
if (!$emp->exists()) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
}
$managers = $database->select('managers', 'managerid', ['employeeid' => $emp->getUID()]);
exit(json_encode(["status" => "OK", "managers" => $managers]));
break;
case "usersearch":
@@ -241,29 +213,23 @@ switch ($VARS['action']) {
exit(json_encode(["status" => "OK", "result" => $data]));
break;
case "permission":
if (is_empty($VARS['code'])) {
if (empty($VARS['code'])) {
http_response_code(400);
die("\"400 Bad Request\"");
}
$perm = $VARS['code'];
if ($VARS['uid']) {
if ($database->has("accounts", ['uid' => $VARS['uid']])) {
$user = $database->select('accounts', ['username'], ['uid' => $VARS['uid']])[0]['username'];
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
}
} else if ($VARS['username']) {
if ($database->has("accounts", ['username' => strtolower($VARS['username'])])) {
$user = $VARS['username'];
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
}
if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
} else if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
$hasperm = account_has_permission($user, $perm);
exit(json_encode(["status" => "OK", "has_permission" => $hasperm]));
if (!$user->exists()) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
}
exit(json_encode(["status" => "OK", "has_permission" => $user->hasPermission($perm)]));
break;
case "mobileenabled":
exit(json_encode(["status" => "OK", "mobile" => MOBILE_ENABLED]));
@@ -277,7 +243,7 @@ switch ($VARS['action']) {
exit(json_encode(["status" => "OK", "valid" => $user_key_valid]));
case "alertemail":
engageRateLimit();
if (is_empty($VARS['username']) || !user_exists($VARS['username'])) {
if (is_empty($VARS['username']) || !User::byUsername($VARS['username'])->exists()) {
http_response_code(400);
die("\"400 Bad Request\"");
}
@@ -285,7 +251,7 @@ switch ($VARS['action']) {
if (!is_empty($VARS['appname'])) {
$appname = $VARS['appname'];
}
$result = sendLoginAlertEmail($VARS['username'], $appname);
$result = User::byUsername($VARS['username'])->sendAlertEmail($appname);
if ($result === TRUE) {
exit(json_encode(["status" => "OK"]));
}
@@ -371,22 +337,19 @@ switch ($VARS['action']) {
http_response_code(400);
die("\"400 Bad Request\"");
}
if (!is_empty($VARS['username'])) {
if (user_exists_local($VARS['username'])) {
$pin = $database->get("accounts", "pin", ["username" => strtolower($VARS['username'])]);
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
}
} else if (!is_empty($VARS['uid'])) {
if ($database->has('accounts', ['uid' => $VARS['uid']])) {
$pin = $database->get("accounts", "pin", ["uid" => strtolower($VARS['uid'])]);
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
}
if (!empty($VARS['username'])) {
$user = User::byUsername($VARS['username']);
} else if (!empty($VARS['uid'])) {
$user = new User($VARS['uid']);
} else {
http_response_code(400);
die("\"400 Bad Request\"");
}
if ($user->exists()) {
$pin = $database->get("accounts", "pin", ["uid" => $user->getUID()]);
} else {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
}
if (is_null($pin) || $pin == "") {
exit(json_encode(["status" => "ERROR", "pinvalid" => false, "nopinset" => true]));
}
@@ -395,8 +358,4 @@ switch ($VARS['action']) {
default:
http_response_code(404);
die(json_encode("404 Not Found: the requested action is not available."));
}
/* } catch (Exception $e) {
header("HTTP/1.1 500 Internal Server Error");
die("\"500 Internal Server Error\"");
} */
}

+ 197
- 0
app.php View File

@@ -0,0 +1,197 @@
<?php

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

require_once __DIR__ . "/required.php";

if ($_SESSION['loggedin'] != true) {
header('Location: index.php');
die("Session expired. Log in again to continue.");
}

require_once __DIR__ . "/pages.php";

$pageid = "home";
if (isset($_GET['page']) && !is_empty($_GET['page'])) {
$pg = strtolower($_GET['page']);
$pg = preg_replace('/[^0-9a-z_]/', "", $pg);
if (array_key_exists($pg, PAGES) && file_exists(__DIR__ . "/pages/" . $pg . ".php")) {
$pageid = $pg;
} else {
$pageid = "404";
}
}

header("Link: <static/fonts/Roboto.css>; rel=preload; as=style", false);
header("Link: <static/css/bootstrap.min.css>; rel=preload; as=style", false);
header("Link: <static/css/material-color/material-color.min.css>; rel=preload; as=style", false);
header("Link: <static/css/app.css>; rel=preload; as=style", false);
header("Link: <static/css/fa-svg-with-js.css>; rel=preload; as=style", false);
header("Link: <static/js/fontawesome-all.min.js>; rel=preload; as=script", false);
header("Link: <static/js/jquery-3.3.1.min.js>; rel=preload; as=script", false);
header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">

<title><?php echo SITE_TITLE; ?></title>

<link rel="icon" href="static/img/logo.svg">

<link href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/material-color/material-color.min.css" rel="stylesheet">
<link href="static/css/app.css" rel="stylesheet">
<link href="static/css/fa-svg-with-js.css" rel="stylesheet">
<script nonce="<?php echo $SECURE_NONCE; ?>">
FontAwesomeConfig = {autoAddCss: false}
</script>
<script src="static/js/fontawesome-all.min.js"></script>
<?php
// custom page styles
if (isset(PAGES[$pageid]['styles'])) {
foreach (PAGES[$pageid]['styles'] as $style) {
echo "<link href=\"$style\" rel=\"stylesheet\">\n";
header("Link: <$style>; rel=preload; as=style", false);
}
}
?>
</head>
<body>

<?php
// Alert messages
if (isset($_GET['msg']) && !is_empty($_GET['msg']) && array_key_exists($_GET['msg'], MESSAGES)) {
// optional string generation argument
if (!isset($_GET['arg']) || is_empty($_GET['arg'])) {
$alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false);
} else {
$alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
}
$alerttype = MESSAGES[$_GET['msg']]['type'];
$alerticon = "square-o";
switch (MESSAGES[$_GET['msg']]['type']) {
case "danger":
$alerticon = "times";
break;
case "warning":
$alerticon = "exclamation-triangle";
break;
case "info":
$alerticon = "info-circle";
break;
case "success":
$alerticon = "check";
break;
}
echo <<<END
<div class="row justify-content-center" id="msg-alert-box">
<div class="col-11 col-sm-6 col-md-5 col-lg-4 col-xl-4">
<div class="alert alert-dismissible alert-$alerttype mt-2 p-0 border-0 shadow">
<div class="p-2 pl-3">
<button type="button" class="close">&times;</button>
<i class="fas fa-$alerticon"></i> $alertmsg
</div>
<div class="progress">
<div class="progress-bar bg-$alerttype w-0" id="msg-alert-timeout-bar"></div>
</div>
</div>
</div>
</div>
END;
}
?>

<?php
// Adjust as needed
$navbar_breakpoint = "md";

// For mobile app
echo "<script nonce=\"$SECURE_NONCE\">var navbar_breakpoint = \"$navbar_breakpoint\";</script>"
?>
<nav class="navbar navbar-expand-<?php echo $navbar_breakpoint; ?> navbar-light bg-orange fixed-top">
<button class="navbar-toggler my-0 py-0" type="button" data-toggle="collapse" data-target="#navbar-collapse" aria-controls="navbar-collapse" aria-expanded="false" aria-label="Toggle navigation">
<!--<i class="fas fa-bars"></i>-->
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand py-0 mr-auto" href="app.php">
<img src="static/img/logo.svg" alt="" class="d-none d-<?php echo $navbar_breakpoint; ?>-inline brand-img py-0" />
<?php echo SITE_TITLE; ?>
</a>

<div class="collapse navbar-collapse py-0" id="navbar-collapse">
<div class="navbar-nav mr-auto py-0">
<?php
$curpagefound = false;
foreach (PAGES as $id => $pg) {
if (isset($pg['navbar']) && $pg['navbar'] === TRUE) {
if ($pageid == $id) {
$curpagefound = true;
?>
<span class="nav-item py-<?php echo $navbar_breakpoint; ?>-0 active">
<?php
} else {
?>
<span class="nav-item py-<?php echo $navbar_breakpoint; ?>-0">
<?php
}
?>
<a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="app.php?page=<?php echo $id; ?>">
<?php
if (isset($pg['icon'])) {
?><i class="<?php echo $pg['icon']; ?> fa-fw"></i> <?php
}
$Strings->get($pg['title']);
?>
</a>
</span>
<?php
}
}
?>
</div>
<div class="navbar-nav ml-auto py-0" id="navbar-right">
<span class="nav-item py-<?php echo $navbar_breakpoint; ?>-0">
<a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="app.php">
<i class="fas fa-user fa-fw"></i><span>&nbsp;<?php echo $_SESSION['realname'] ?></span>
</a>
</span>
<span class="nav-item mr-auto py-<?php echo $navbar_breakpoint; ?>-0">
<a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="action.php?action=signout">
<i class="fas fa-sign-out-alt fa-fw"></i><span>&nbsp;<?php $Strings->get("sign out") ?></span>
</a>
</span>
</div>
</div>
</nav>

<div class="container" id="main-content">
<div>
<?php
include_once __DIR__ . '/pages/' . $pageid . ".php";
?>
</div>
<div class="footer">
<?php echo FOOTER_TEXT; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
</div>
</div>
<script src="static/js/jquery-3.3.1.min.js"></script>
<script src="static/js/bootstrap.min.js"></script>
<script src="static/js/app.js"></script>
<?php
// custom page scripts
if (isset(PAGES[$pageid]['scripts'])) {
foreach (PAGES[$pageid]['scripts'] as $script) {
echo "<script src=\"$script\"></script>\n";
header("Link: <$script>; rel=preload; as=script", false);
}
}
?>
</body>
</html>

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

@@ -1,13 +0,0 @@
<?php

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

dieifnotloggedin();

$APPS["404_error"]["title"] = $Strings->get("404 error", false);
$APPS["404_error"]["icon"] = "times";
$APPS["404_error"]["type"] = "warning";
$APPS["404_error"]["content"] = "<h4>" . $Strings->get("page not found", false) . "</h4>";
?>

+ 0
- 26
apps/change_password.php View File

@@ -1,26 +0,0 @@
<?php

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

dieifnotloggedin();

$oldpass = $Strings->get("current password", false);
$newpass = $Strings->get("new password", false);
$conpass = $Strings->get("confirm password", false);
$change = $Strings->get("change password", false);

$APPS["change_password"]["title"] = $Strings->get("change password", false);
$APPS["change_password"]["icon"] = "key";
$APPS["change_password"]["content"] = <<<CONTENTEND
<form action="action.php" method="POST">
<input type="password" class="form-control" name="oldpass" placeholder="$oldpass" />
<input type="password" class="form-control" name="newpass" placeholder="$newpass" />
<input type="password" class="form-control" name="conpass" placeholder="$conpass" />
<input type="hidden" name="action" value="chpasswd" />
<input type="hidden" name="source" value="security" />
<br />
<button type="submit" class="btn btn-success btn-sm btn-block">$change</button>
</form>
CONTENTEND;

+ 0
- 27
apps/change_pin.php View File

@@ -1,27 +0,0 @@
<?php

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

dieifnotloggedin();

$newpin = $Strings->get("new pin", false);
$conpin = $Strings->get("confirm pin", false);
$change = $Strings->get("change pin", false);
$pinexp = $Strings->get("pin explanation", false);


$APPS["change_pin"]["title"] = $Strings->get("change pin", false);
$APPS["change_pin"]["icon"] = "th";
$APPS["change_pin"]["content"] = <<<CONTENTEND
<div class="alert alert-info"><i class="fa fa-info-circle"></i> $pinexp</div>
<form action="action.php" method="POST">
<input type="password" class="form-control" name="newpin" placeholder="$newpin" maxlength="8" pattern="[0-9]*" inputmode="numeric" />
<input type="password" class="form-control" name="conpin" placeholder="$conpin" maxlength="8" pattern="[0-9]*" inputmode="numeric" />
<input type="hidden" name="action" value="chpin" />
<input type="hidden" name="source" value="security" />
<br />
<button type="submit" class="btn btn-success btn-sm btn-block">$change</button>
</form>
CONTENTEND;

+ 0
- 79
apps/setup_2fa.php View File

@@ -1,79 +0,0 @@
<?php

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

dieifnotloggedin();

use OTPHP\Factory;
use Endroid\QrCode\ErrorCorrectionLevel;
use Endroid\QrCode\QrCode;

// extra login utils
require_once __DIR__ . "/../lib/login.php";

$APPS["setup_2fa"]["title"] = $Strings->get("setup 2fa", false);
$APPS["setup_2fa"]["icon"] = "lock";
if (userHasTOTP($_SESSION['username'])) {
$APPS["setup_2fa"]["content"] = '<div class="alert alert-info"><i class="fa fa-info-circle"></i> ' . $Strings->get("2fa active", false) . '</div>'
. '<a href="action.php?action=rm2fa&source=security" class="btn btn-warning btn-sm btn-block">'
. $Strings->get("remove 2fa", false) . '</a>';
} else if ($_GET['2fa'] == "generate") {
$codeuri = newTOTP($_SESSION['username']);
$userdata = $database->select('accounts', ['email', 'authsecret', 'realname'], ['username' => $_SESSION['username']])[0];
$label = SYSTEM_NAME . ":" . is_null($userdata['email']) ? $userdata['realname'] : $userdata['email'];
$issuer = SYSTEM_NAME;
$qrCode = new QrCode($codeuri);
$qrCode->setWriterByName('svg');
$qrCode->setSize(550);
$qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH);
$qrcode = $qrCode->writeDataUri();
$totp = Factory::loadFromProvisioningUri($codeuri);
$codesecret = $totp->getSecret();
$chunk_secret = trim(chunk_split($codesecret, 4, ' '));
$lang_manualsetup = $Strings->get("manual setup", false);
$lang_secretkey = $Strings->get("secret key", false);
$lang_label = $Strings->get("label", false);
$lang_issuer = $Strings->get("issuer", false);
$lang_entercode = $Strings->get("enter otp code", false);
$APPS["setup_2fa"]["content"] = '<div class="alert alert-info"><i class="fa fa-info-circle"></i> ' . $Strings->get("scan 2fa qrcode", false) . '</div>' . <<<END
<style nonce="$SECURE_NONCE">
.margintop-15px {
margin-top: 15px;
}
.mono-chunk {
text-align: center;
font-size: 110%;
font-family: monospace;
}
</style>
<img src="$qrcode" class="img-responsive qrcode" />
<form action="action.php" method="POST" class="margintop-15px">
<input type="text" name="totpcode" class="form-control" placeholder="$lang_entercode" minlength=6 maxlength=6 required />
<br />
<input type="hidden" name="action" value="add2fa" />
<input type="hidden" name="source" value="security" />
<input type="hidden" name="secret" value="$codesecret" />
<button type="submit" class="btn btn-success btn-sm btn-block">
END
. $Strings->get("confirm 2fa", false) . <<<END
</button>
</form>
<div class="panel panel-default margintop-15px">
<div class="panel-body">
<b>$lang_manualsetup</b>
<br /><label>$lang_secretkey:</label>
<div class="well well-sm mono-chunk">$chunk_secret</div>
<br /><label>$lang_label:</label>
<div class="well well-sm mono-chunk">$label</div>
<br /><label>$lang_issuer:</label>
<div class="well well-sm mono-chunk">$issuer</div>
</div>
</div>
END;
} else {
$APPS["setup_2fa"]["content"] = '<div class="alert alert-info"><i class="fa fa-info-circle"></i> ' . $Strings->get("2fa explained", false) . '</div>'
. '<a class="btn btn-success btn-sm btn-block" href="home.php?page=security&2fa=generate">'
. $Strings->get("enable 2fa", false) . '</a>';
}

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 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

require_once __DIR__ . "/required.php";

if ($_SESSION['loggedin'] != true) {
header('Location: index.php');
die("Session expired. Log in again to continue.");
} else if (is_empty($_SESSION['password'])) {
header('Location: index.php');
die("You need to log in again.");
}

require_once __DIR__ . "/pages.php";

$pageid = "home";
if (!is_empty($_GET['page'])) {
if (array_key_exists($_GET['page'], PAGES)) {
$pageid = $_GET['page'];
} else {
$pageid = "404";
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">

<title><?php echo SITE_TITLE; ?></title>

<link href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/font-awesome.min.css" rel="stylesheet">
<link href="static/css/material-color/material-color.min.css" rel="stylesheet">
<link href="static/css/app.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-4 col-sm-offset-3 col-md-offset-4 col-lg-offset-4">
<?php
if ((SHOW_ICON == "both" || SHOW_ICON == "app") && ICON_POSITION != "menu") {
if (MENU_BAR_STYLE != "fixed") {
?>
<img class="img-responsive banner-image" src="static/img/logo.svg" />
<?php
}
}
?>
</div>
</div>
<nav class="navbar navbar-default navbar-orange navbar-<?php echo MENU_BAR_STYLE; ?>-top">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<?php
if (SHOW_ICON == "both" || SHOW_ICON == "app") {
if (MENU_BAR_STYLE == "fixed" || ICON_POSITION == "menu") {
$src = "static/img/logo.svg";
if ($pageid != "home") {
$src = "static/img/up-arrow-black.png";
}
?>
<a class="navbar-brand" href="home.php">
<img src="<?php echo $src; ?>" />
</a>
<?php
}
}
?>
<a class="navbar-brand" href="home.php">
<?php
echo SITE_TITLE;
?>
</a>
</div>

<div class="collapse navbar-collapse" id="navbar-collapse">
<ul class="nav navbar-nav">
<?php
$counter = 0;
$more = "";
$curpagefound = false;
foreach (PAGES as $id => $pg) {
if ($pg['navbar'] === TRUE) {
$counter++;
if ($counter > ($curpagefound ? 4 : 3) && $pageid != $id) {
$item = '<a href="home.php?page=' . $id . '">';
if (isset($pg['icon'])) {
$item .= '<i class="fa fa-' . $pg['icon'] . ' fa-fw"></i>';
}
$item .= $Strings->get($pg['title'], false) . '</a>';
echo '<li class="hidden-sm hidden-md">' . $item . "</li>";
$more .= '<li>' . $item . "</li>";
} else {
if ($pageid == $id) {
$curpagefound = true;
?>
<li class="active">
<?php
} else {
?>
<li>
<?php } ?>
<a href="home.php?page=<?php echo $id; ?>">
<?php
if (isset($pg['icon'])) {
?>
<i class="fa fa-<?php echo $pg['icon']; ?> fa-fw"></i>
<?php } ?>
<?php $Strings->get($pg['title']) ?>
</a>
</li>
<?php
}
}
}

if ($counter > 4) {
?>
<li class="dropdown hidden-lg hidden-xs">
<a href="" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-ellipsis-v fa-fw"></i> <?php $Strings->get("more"); ?></a>
<ul class="dropdown-menu"><?php echo $more; ?></ul>
</li>
<?php } ?>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="home.php"><i class="fa fa-user fa-fw"></i> <span class=""><?php echo $_SESSION['realname'] ?></span></a></li>
<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>
</ul>
</div>
</nav>
<?php
if (MENU_BAR_STYLE == "fixed") {
?>
<div class="pad-75px"></div>
<?php
}
?>

<div class="app-dock-container mobile-app-hide">
<div class="app-dock">
<?php
foreach (EXTERNAL_APPS as $a) {
?>
<div class="app-dock-item">
<p>
<a href="<?php echo $a['url']; ?>">
<img class="img-responsive app-icon" src="<?php
if (strpos($a['icon'], "http") !== 0) {
echo $a['url'] . $a['icon'];
} else {
echo $a['icon'];
}
?>"/>
<span><?php echo $a['title']; ?></span>
</a>
</p>
</div>
<?php
}
?>
</div>
</div>

<?php
// Alert messages
if (!is_empty($_GET['msg']) && array_key_exists($_GET['msg'], MESSAGES)) {
// optional string generation argument
if (is_empty($_GET['arg'])) {
$alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false);
} else {
$alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => $_GET['arg']], false);
}
$alerttype = MESSAGES[$_GET['msg']]['type'];
$alerticon = "square-o";
switch (MESSAGES[$_GET['msg']]['type']) {
case "danger":
$alerticon = "times";
break;
case "warning":
$alerticon = "exclamation-triangle";
break;
case "info":
$alerticon = "info-circle";
break;
case "success":
$alerticon = "check";
break;
}
echo <<<END
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-4 col-sm-offset-3 col-md-offset-4 col-lg-offset-4">
<div class="alert alert-dismissible alert-$alerttype">
<button type="button" class="close">&times;</button>
<i class="fa fa-$alerticon"></i> $alertmsg
</div>
</div>
</div>
END;
}
?>
<div class="row widget-box">
<?php
// Center the widgets horizontally on the screen
$appcount = 0;
foreach (APPS[$pageid] as $app) {
if (file_exists(__DIR__ . "/apps/" . $app . ".php")) {
include_once __DIR__ . "/apps/" . $app . ".php";
if (isset($APPS[$app])) {
$appcount++;
}
}
}
if ($appcount == 1) {
?>
<div class="hidden-xs col-sm-3 col-md-4 col-lg-4">
<!-- Empty placeholder column for nice center-align -->
</div>
<?php
} else if ($appcount == 2) {
?>
<div class="hidden-xs hidden-sm col-md-2 col-lg-2">
<!-- Empty placeholder column for nice center-align -->
</div>
<?php
}

// Load app widgets
foreach (APPS[$pageid] as $app) {
if (file_exists(__DIR__ . "/apps/" . $app . ".php")) {
include_once __DIR__ . "/apps/" . $app . ".php";
if (!isset($APPS[$app])) {
continue;
}
$apptitle = ($APPS[$app]['i18n'] === TRUE ? $Strings->get($APPS[$app]['title'], false) : $APPS[$app]['title']);
$appicon = (is_empty($APPS[$app]['icon']) ? "" : "fa fa-fw fa-" . $APPS[$app]['icon']);
$apptype = (is_empty($APPS[$app]['type']) ? "default" : $APPS[$app]['type']);
$appcontent = $APPS[$app]['content'];
echo <<<END
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-4">
<div class="panel panel-$apptype apppanel">
<div class="panel-heading">
<h3 class="panel-title"><i class="$appicon"></i> $apptitle </h3>
</div>
<div class="panel-body">
$appcontent
</div>
</div>
</div>
END;
}
}
?>
</div>
<div class="footer">
<?php echo FOOTER_TEXT; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
</div>
</div>
<script src="static/js/jquery-3.2.1.min.js"></script>
<script src="static/js/bootstrap.min.js"></script>
<script src="static/js/app.js"></script>
</body>
</html>
header("Location: app.php");

+ 133
- 138
index.php View File

@@ -5,28 +5,26 @@

require_once __DIR__ . "/required.php";

require_once __DIR__ . "/lib/login.php";

// If we're logged in, we don't need to be here.
if ($_SESSION['loggedin'] && !is_empty($_SESSION['password'])) {
if (!empty($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
header('Location: home.php');
die();
// This branch will likely run if the user signed in from a different app.
} else if ($_SESSION['loggedin'] && is_empty($_SESSION['password'])) {
$alert = $Strings->get("sign in again", false);
$alerttype = "info";
}

/* Authenticate user */
$username_ok = false;
$multiauth = false;
$change_password = false;
if ($VARS['progress'] == "1") {
if (empty($VARS['progress'])) {
// Easy way to remove "undefined" warnings.
} else if ($VARS['progress'] == "1") {
engageRateLimit();
if (!CAPTCHA_ENABLED || (CAPTCHA_ENABLED && verifyCaptcheck($VARS['captcheck_session_code'], $VARS['captcheck_selected_answer'], CAPTCHA_SERVER . "/api.php"))) {
if (!CAPTCHA_ENABLED || (CAPTCHA_ENABLED && Login::verifyCaptcha($VARS['captcheck_session_code'], $VARS['captcheck_selected_answer'], CAPTCHA_SERVER . "/api.php"))) {
$autherror = "";
if (user_exists($VARS['username'])) {
$status = get_account_status($VARS['username'], $error);
$user = User::byUsername($VARS['username']);
if ($user->exists()) {
$status = $user->getStatus()->getString();
switch ($status) {
case "LOCKED_OR_DISABLED":
$alert = $Strings->get("account locked", false);
@@ -37,15 +35,15 @@ if ($VARS['progress'] == "1") {
case "CHANGE_PASSWORD":
$alert = $Strings->get("password expired", false);
$alerttype = "info";
$_SESSION['username'] = strtolower($VARS['username']);
$_SESSION['uid'] = $database->get('accounts', 'uid', ['username' => strtolower($VARS['username'])]);
$_SESSION['username'] = $user->getUsername();
$_SESSION['uid'] = $user->getUID();
$change_password = true;
break;
case "NORMAL":
$username_ok = true;
break;
case "ALERT_ON_ACCESS":
$mail_resp = sendLoginAlertEmail($VARS['username']);
$mail_resp = $user->sendAlertEmail();
if (DEBUG) {
var_dump($mail_resp);
}
@@ -60,73 +58,69 @@ if ($VARS['progress'] == "1") {
break;
}
if ($username_ok) {
if (authenticate_user($VARS['username'], $VARS['password'], $autherror)) {
if ($user->checkPassword($VARS['password'])) {
$_SESSION['passok'] = true; // stop logins using only username and authcode
if (userHasTOTP($VARS['username'])) {
if ($user->has2fa()) {
$multiauth = true;
$_SESSION['password'] = $VARS['password'];
} else {
doLoginUser($VARS['username'], $VARS['password']);
insertAuthLog(1, $_SESSION['uid']);
header('Location: home.php');
die("Logged in, go to home.php");
Session::start($user);
Log::insert(LogType::LOGIN_OK, $user->getUID());
header('Location: app.php');
die("Logged in, go to app.php");
}
} else {
if (!is_empty($autherror)) {
$alert = $autherror;
insertAuthLog(2, null, "Username: " . $VARS['username']);
} else {
$alert = $Strings->get("login incorrect", false);
insertAuthLog(2, null, "Username: " . $VARS['username']);
}
$alert = $Strings->get("login incorrect", false);
Log::insert(LogType::LOGIN_FAILED, null, "Username: " . $VARS['username']);
}
}
} else { // User does not exist anywhere
$alert = $Strings->get("login incorrect", false);
insertAuthLog(2, null, "Username: " . $VARS['username']);
Log::insert(LogType::LOGIN_FAILED, null, "Username: " . $VARS['username']);
}
} else {
$alert = $Strings->get("captcha error", false);
insertAuthLog(8, null, "Username: " . $VARS['username']);
Log::insert(LogType::BAD_CAPTCHA, null, "Username: " . $VARS['username']);
}
} else if ($VARS['progress'] == "2") {
engageRateLimit();
$user = User::byUsername($VARS['username']);
if ($_SESSION['passok'] !== true) {
// stop logins using only username and authcode
sendError("Password integrity check failed!");
}
if (verifyTOTP($VARS['username'], $VARS['authcode'])) {
doLoginUser($VARS['username'], $VARS['password']);
insertAuthLog(1, $_SESSION['uid']);
header('Location: home.php');
die("Logged in, go to home.php");
if ($user->check2fa($VARS['authcode'])) {
Session::start($user);
Log::insert(LogType::LOGIN_OK, $user->getUID());
header('Location: app.php');
die("Logged in, go to app.php");
} else {
$alert = $Strings->get("2fa incorrect", false);
insertAuthLog(6, null, "Username: " . $VARS['username']);
Log::insert(LogType::BAD_2FA, null, "Username: " . $VARS['username']);
}
} else if ($VARS['progress'] == "chpasswd") {
engageRateLimit();
if (!is_empty($_SESSION['username'])) {
$error = [];
$result = change_password($VARS['oldpass'], $VARS['newpass'], $VARS['conpass'], $error);
if ($result === TRUE) {
$alert = $Strings->get(MESSAGES["password_updated"]["string"], false);
$alerttype = MESSAGES["password_updated"]["type"];
}
switch (count($error)) {
case 0:
break;
case 1:
$alert = $Strings->get(MESSAGES[$error[0]]["string"], false);
$alerttype = MESSAGES[$error[0]]["type"];
break;
case 2:
$alert = $Strings->build(MESSAGES[$error[0]]["string"], ["arg" => $error[1]], false);
$alerttype = MESSAGES[$error[0]]["type"];
break;
default:
$alert = $Strings->get(MESSAGES["generic_op_error"]["string"], false);
$alerttype = MESSAGES["generic_op_error"]["type"];
$user = User::byUsername($_SESSION['username']);

try {
$result = $user->changePassword($VARS['oldpass'], $VARS['newpass'], $VARS['conpass']);

if ($result === TRUE) {
$alert = $Strings->get(MESSAGES["password_updated"]["string"], false);
$alerttype = MESSAGES["password_updated"]["type"];
}
} catch (PasswordMatchException $e) {
$alert = $Strings->get(MESSAGES["passwords_same"]["string"], false);
$alerttype = "danger";
} catch (PasswordMismatchException $e) {
$alert = $Strings->get(MESSAGES["new_password_mismatch"]["string"], false);
$alerttype = "danger";
} catch (IncorrectPasswordException $e) {
$alert = $Strings->get(MESSAGES["old_password_mismatch"]["string"], false);
$alerttype = "danger";
} catch (WeakPasswordException $e) {
$alert = $Strings->get(MESSAGES["weak_password"]["string"], false);
$alerttype = "danger";
}
} else {
session_destroy();
@@ -134,6 +128,13 @@ if ($VARS['progress'] == "1") {
die();
}
}

header("Link: <static/fonts/Roboto.css>; rel=preload; as=style", false);
header("Link: <static/css/bootstrap.min.css>; rel=preload; as=style", false);
header("Link: <static/css/material-color/material-color.min.css>; rel=preload; as=style", false);
header("Link: <static/css/index.css>; rel=preload; as=style", false);
header("Link: <static/js/jquery-3.3.1.min.js>; rel=preload; as=script", false);
header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
?>
<!DOCTYPE html>
<html>
@@ -144,101 +145,95 @@ if ($VARS['progress'] == "1") {

<title><?php echo SITE_TITLE; ?></title>

<link rel="icon" href="static/img/logo.svg">

<link href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/font-awesome.min.css" rel="stylesheet">
<link href="static/css/material-color/material-color.min.css" rel="stylesheet">
<link href="static/css/app.css" rel="stylesheet">
<link href="static/css/index.css" rel="stylesheet">
<?php if (CAPTCHA_ENABLED) { ?>
<script src="<?php echo CAPTCHA_SERVER ?>/captcheck.dist.js"></script>
<?php } ?>
<?php } ?>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-4 col-sm-offset-3 col-md-offset-4 col-lg-offset-4">
<div>
<div class="row justify-content-center">
<div class="col-auto">
<img class="banner-image" src="static/img/logo.svg" />
</div>
</div>
<div class="row justify-content-center">
<div class="card col-11 col-xs-11 col-sm-8 col-md-6 col-lg-4">
<div class="card-body">
<h5 class="card-title"><?php $Strings->get("sign in"); ?></h5>
<form action="" method="POST">
<?php
if (SHOW_ICON == "both" || SHOW_ICON == "index") {
if (!empty($alert)) {
$alerttype = isset($alerttype) ? $alerttype : "danger";
?>
<img class="img-responsive banner-image" src="static/img/logo.svg" />
<?php } ?>
</div>
<div class="panel panel-orange">
<div class="panel-heading">
<h3 class="panel-title"><?php $Strings->get("sign in"); ?></h3>
</div>
<div class="panel-body">
<form action="" method="POST">
<div class="alert alert-<?php echo $alerttype ?>">
<?php
if (!is_empty($alert)) {
$alerttype = isset($alerttype) ? $alerttype : "danger";
?>
<div class="alert alert-<?php echo $alerttype ?>">
<?php
switch ($alerttype) {
case "danger":
$alerticon = "times";
break;
case "warning":
$alerticon = "exclamation-triangle";
break;
case "info":
$alerticon = "info-circle";
break;
case "success":
$alerticon = "check";
break;
default:
$alerticon = "square-o";
}
?>
<i class="fa fa-fw fa-<?php echo $alerticon ?>"></i> <?php echo $alert ?>
</div>
<?php
}

if (!$multiauth && !$change_password) {
?>
<input type="text" class="form-control" name="username" placeholder="<?php $Strings->get("username"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="password" class="form-control" name="password" placeholder="<?php $Strings->get("password"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
<?php if (CAPTCHA_ENABLED) { ?>
<div class="captcheck_container" data-stylenonce="<?php echo $SECURE_NONCE; ?>"></div>
<br />
<?php } ?>
<input type="hidden" name="progress" value="1" />
<?php
} else if ($multiauth) {
?>
<div class="alert alert-info">
<?php $Strings->get("2fa prompt"); ?>
</div>
<input type="text" class="form-control" name="authcode" placeholder="<?php $Strings->get("authcode"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="hidden" name="progress" value="2" />
<input type="hidden" name="username" value="<?php echo $VARS['username']; ?>" />
<?php
} else if ($change_password) {
?>
<input type="password" class="form-control" name="oldpass" placeholder="Current password" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="password" class="form-control" name="newpass" placeholder="New password" required="required" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
<input type="password" class="form-control" name="conpass" placeholder="New password (again)" required="required" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
<input type="hidden" name="progress" value="chpasswd" />
<?php
switch ($alerttype) {
case "danger":
$alerticon = "fas fa-times";
break;
case "warning":
$alerticon = "fas fa-exclamation-triangle";
break;
case "info":
$alerticon = "fas fa-info-circle";
break;
case "success":
$alerticon = "fas fa-check";
break;
default:
$alerticon = "far fa-square";
}
?>
<button type="submit" class="btn btn-primary">
<?php $Strings->get("continue"); ?>
</button>
</form>
</div>
</div>
<i class="<?php echo $alerticon ?> fa-fw"></i> <?php echo $alert ?>
</div>
<?php
}

if (!$multiauth && !$change_password) {
?>
<input type="text" class="form-control" name="username" placeholder="<?php $Strings->get("username"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="password" class="form-control" name="password" placeholder="<?php $Strings->get("password"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
<?php if (CAPTCHA_ENABLED) { ?>
<div class="captcheck_container" data-stylenonce="<?php echo $SECURE_NONCE; ?>"></div>
<br />
<?php } ?>
<input type="hidden" name="progress" value="1" />
<?php
} else if ($multiauth) {
?>
<div class="alert alert-info">
<?php $Strings->get("2fa prompt"); ?>
</div>
<input type="text" class="form-control" name="authcode" placeholder="<?php $Strings->get("authcode"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="hidden" name="progress" value="2" />
<input type="hidden" name="username" value="<?php echo $VARS['username']; ?>" />
<?php
} else if ($change_password) {
?>
<input type="password" class="form-control" name="oldpass" placeholder="Current password" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="password" class="form-control" name="newpass" placeholder="New password" required="required" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
<input type="password" class="form-control" name="conpass" placeholder="New password (again)" required="required" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
<input type="hidden" name="progress" value="chpasswd" />
<?php
}
?>
<button type="submit" class="btn btn-primary">
<?php $Strings->get("continue"); ?>
</button>
</form>
</div>
</div>
<div class="footer">
<?php echo FOOTER_TEXT; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
</div>
</div>
<script src="static/js/jquery-3.2.1.min.js"></script>
<script src="static/js/bootstrap.min.js"></script>
</body>
<div class="footer">
<?php echo FOOTER_TEXT; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
</div>
</div>
<script src="static/js/jquery-3.3.1.min.js"></script>
<script src="static/js/bootstrap.min.js"></script>
</body>
</html>

+ 0
- 110
lang/en_us.php View File

@@ -1,110 +0,0 @@
<?php

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

$STRINGS = [
"sign in" => "Sign In",
"username" => "Username",
"password" => "Password",
"continue" => "Continue",
"authcode" => "Authentication code",
"2fa prompt" => "Enter the six-digit code from your mobile authenticator "
. "app.",
"2fa incorrect" => "Authentication code incorrect.",
"login incorrect" => "Login incorrect.",
"login successful" => "Login successful.",
"login error" => "There was a server problem. Try again later.",
"account locked" => "This account has been disabled. Contact technical "
. "support.",
"password expired" => "You must change your password before continuing.",
"account terminated" => "Account terminated. Access denied.",
"account state error" => "Your account state is not stable. Log out, "
. "restart your browser, and try again.",
"password on 500 list" => "The given password is ranked number {arg} out "
. "of the 500 most common passwords. Try a different one.",
"welcome user" => "Welcome, {user}!",
"change password" => "Change password",
"account security" => "Account security",
"security options" => "Security options",
"account options" => "Account options",
"sync" => "Sync settings",
"options" => "Options",
"sign out" => "Sign out",
"settings" => "Settings",
"account" => "Account",
"404 error" => "404 Error",
"page not found" => "Page not found.",
"current password incorrect" => "The current password is incorrect. "
. "Try again.",
"new password mismatch" => "The new passwords did not match. Try again.",
"weak password" => "Password does not meet requirements.",
"password updated" => "Password updated successfully.",
"current password" => "Current password",
"new password" => "New password",
"confirm password" => "New password (again)",
"setup 2fa" => "Setup 2-factor authentication",
"2fa removed" => "2-factor authentication disabled.",
"2fa enabled" => "2-factor authentication activated.",
"remove 2fa" => "Disable 2-factor authentication",
"2fa explained" => "2-factor authentication adds more security to your "
. "account. You can use the Auth Keys (key icon) feature of the Netsyms "
. "Business Mobile app, or another TOTP-enabled app (Authy, FreeOTP, etc) on your "
. "smartphone. When you have the app installed, you can enable 2-factor "
. "authentication by clicking the button below and scanning a QR code with "
. "the app. Whenever you sign in in the future, you'll need to input a "
. "six-digit code from your phone into the login page when prompted. You "
. "can disable 2-factor authentication from this page if you change your "
. "mind.",
"2fa active" => "2-factor authentication is active on your account. To "
. "remove 2fa, reset your authentication secret, or change to a new "
. "security device, click the button below.",
"enable 2fa" => "Enable 2-factor authentication",
"scan 2fa qrcode" => "Scan the QR Code with the authenticator app, or enter"
. " the information manually. Then type in the six-digit code the app gives you and press Finish Setup.",
"confirm 2fa" => "Finish setup",
"invalid parameters" => "Invalid request parameters.",
"ldap server error" => "The LDAP server returned an error: {arg}",
"user does not exist" => "User does not exist.",
"captcha error" => "There was a problem with the CAPTCHA (robot test). "
. "Try again.",
"home" => "Home",
"ldap error" => "LDAP error: {error}",
"old and new passwords match" => "Your current and new passwords are the "
. "same.",
"generic op error" => "An unknown error occurred. Try again later.",
"password complexity insufficent" => "The new password does not meet the "
. "minumum requirements defined by your system administrator.",
"error loading widget" => "There was a problem loading this app.",
"open app" => "Open App",
"sign in again" => "Please sign in again to continue.",
"login failed try on web" => "There is a problem with your account. Visit "
. "AccountHub via a web browser for more information.",
"mobile login disabled" => "Mobile login has been disabled by your system "
. "administrator. Contact technical support for more information.",
"admin alert email subject" => "Alert: User login notification",
"admin alert email message" => "You (or another administrator) requested to"
. " be notified when user \"{username}\" logged in, an event which happened"
. " just now."
. "\r\n"
. "\r\nUsername: \t{username}"
. "\r\nApplication: \t{appname}"
. "\r\nDate/Time: \t{datetime}"
. "\r\nIP address: \t{ipaddr}"
. "\r\n"
. "\r\nThese notifications can be disabled by editing the user in "
. "ManagePanel.",
"enter otp code" => "Enter 6-digit code",
"secret key" => "Secret key",
"label" => "Label",
"issuer" => "Issuer",
"no such code or code expired" => "That code is incorrect or expired.",
"pin explanation" => "Change or set a login PIN for the Station kiosk Quick Access. PIN codes must be between one and eight digits.",
"change pin" => "Change PIN",
"new pin" => "New PIN",
"confirm pin" => "New PIN (again)",
"pin updated" => "PIN updated.",
"new pin mismatch" => "The new PINs don't match each other.",
"invalid pin format" => "PIN codes must be numeric and between one and eight digits in length.",
];

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

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

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

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

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


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

@@ -0,0 +1,35 @@
<?php

/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

class IncorrectPasswordException extends Exception {
public function __construct(string $message = "Incorrect password.", int $code = 0, \Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
}

class WeakPasswordException extends Exception {
public function __construct(string $message = "Password is weak or compromised.", int $code = 0, \Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
}

class PasswordMatchException extends Exception {

public function __construct(string $message = "Old and new passwords are identical", int $code = 0, \Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}

}

class PasswordMismatchException extends Exception {

public function __construct(string $message = "Passwords do not match", int $code = 0, \Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}

}

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

@@ -0,0 +1,72 @@
<?php

/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

class Log {

/**
*
* @global $database
* @param int/LogType $type Either an integer (as defined by the constants in class LogType) or a LogType object.
* @param int/User $user Either a UID number or a User object.
* @param string $data Extra data to include in the log, in addition to the timestamp, log type, user, and IP address.
*/
public static function insert($type, $user, string $data = "") {
global $database;
// find IP address
$ip = getClientIP();
if (gettype($type) == "object" && is_a($type, "LogType")) {
$type = $type->getType();
}

if (is_a($user, "User")) {
$uid = $user->getUID();
} else if (gettype($user) == "integer") {
$uid = $user;
} else {
$uid = null;
}

$database->insert("authlog", ['logtime' => date("Y-m-d H:i:s"), 'logtype' => $type, 'uid' => $uid, 'ip' => $ip, 'otherdata' => $data]);
}

}

class LogType {

const LOGIN_OK = 1;
const LOGIN_FAILED = 2;
const PASSWORD_CHANGED = 3;
const API_LOGIN_OK = 4;
const API_LOGIN_FAILED = 5;
const BAD_2FA = 6;
const API_BAD_2FA = 7;
const BAD_CAPTCHA = 8;
const ADDED_2FA = 9;
const REMOVED_2FA = 10;
const LOGOUT = 11;
const API_AUTH_OK = 12;
const API_AUTH_FAILED = 13;
const API_BAD_KEY = 14;
const LOG_CLEARED = 15;
const USER_REMOVED = 16;
const USER_ADDED = 17;
const USER_EDITED = 18;
const MOBILE_LOGIN_OK = 19;
const MOBILE_LOGIN_FAILED = 20;
const MOBILE_BAD_KEY = 21;

private $type;

function __construct(int $type) {
$this->type = $type;
}

public function getType(): int {
return $type;
}
}

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

@@ -0,0 +1,71 @@
<?php

/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

class Login {

const BAD_USERPASS = 1;
const BAD_2FA = 2;
const ACCOUNT_DISABLED = 3;
const LOGIN_OK = 4;

public static function auth(string $username, string $password, string $twofa = ""): int {
global $database;
$username = strtolower($username);

$user = User::byUsername($username);

if (!$user->exists()) {
return Login::BAD_USERPASS;
}
if (!$user->checkPassword($password)) {
return Login::BAD_USERPASS;
}

if ($user->has2fa()) {
if (!$user->check2fa($twofa)) {
return Login::BAD_2FA;
}
}

switch ($user->getStatus()->get()) {
case AccountStatus::TERMINATED:
return Login::BAD_USERPASS;
case AccountStatus::LOCKED_OR_DISABLED:
return Login::ACCOUNT_DISABLED;
case AccountStatus::NORMAL:
default:
return Login::LOGIN_OK;
}

return Login::LOGIN_OK;
}

public static function verifyCaptcha(string $session, string $answer, string $url): bool {
$data = [
'session_id' => $session,
'answer_id' => $answer,
'action' => "verify"
];
$options = [
'http' => [
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($data)
]
];
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
$resp = json_decode($result, TRUE);
if (!$resp['result']) {
return false;
} else {
return true;
}
}

}

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

@@ -0,0 +1,19 @@
<?php

/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

class Session {

public static function start(User $user) {
$_SESSION['username'] = $user->getUsername();
$_SESSION['uid'] = $user->getUID();
$_SESSION['email'] = $user->getEmail();
$_SESSION['realname'] = $user->getName();
$_SESSION['loggedin'] = true;
}

}

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

@@ -14,7 +14,7 @@ class Strings {
private $language = "en";
private $strings = [];

function __construct($language = "en") {
public function __construct($language = "en") {
if (!preg_match("/[a-zA-Z\_\-]+/", $language)) {
throw new Exception("Invalid language code $language");
}
@@ -50,7 +50,7 @@ class Strings {
* Add language strings dynamically.
* @param array $strings ["key" => "value", ...]
*/
function addStrings(array $strings) {
public function addStrings(array $strings) {
foreach ($strings as $key => $val) {
$this->strings[$key] = $val;
}
@@ -62,7 +62,7 @@ class Strings {
* @param bool $echo True to echo the result, false to return it. Default is true.
* @return string
*/
function get(string $key, bool $echo = true): string {
public function get(string $key, bool $echo = true): string {
$str = $key;
if (array_key_exists($key, $this->strings)) {
$str = $this->strings[$key];
@@ -85,7 +85,7 @@ class Strings {
* @param bool $echo True to echo the result, false to return it. Default is true.
* @return string
*/
function build(string $key, array $replace, bool $echo = true): string {
public function build(string $key, array $replace, bool $echo = true): string {
$str = $key;
if (array_key_exists($key, $this->strings)) {
$str = $this->strings[$key];
@@ -107,7 +107,7 @@ class Strings {
* Builds and returns a JSON key:value string for the supplied array of keys.
* @param array $keys ["key1", "key2", ...]
*/
function getJSON(array $keys): string {
public function getJSON(array $keys): string {
$strings = [];
foreach ($keys as $k) {
$strings[$k] = $this->get($k, false);

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

@@ -0,0 +1,307 @@
<?php

/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

use Base32\Base32;
use OTPHP\TOTP;

class User {

private $uid = null;
private $username;
private $passhash;
private $email;
private $realname;
private $authsecret;
private $has2fa = false;
private $exists = false;

public function __construct(int $uid, string $username = "") {
global $database;
if ($database->has('accounts', ['AND' => ['uid' => $uid, 'deleted' => false]])) {
$this->uid = $uid;
$user = $database->get('accounts', ['username', 'password', 'email', 'realname', 'authsecret'], ['uid' => $uid]);
$this->username = $user['username'];
$this->passhash = $user['password'];
$this->email = $user['email'];
$this->realname = $user['realname'];
$this->authsecret = $user['authsecret'];
$this->has2fa = !empty($user['authsecret']);
$this->exists = true;
} else {
$this->uid = $uid;
$this->username = $username;
}
}

public static function byUsername(string $username): User {
global $database;
$username = strtolower($username);
if ($database->has('accounts', ['AND' => ['username' => $username, 'deleted' => false]])) {
$uid = $database->get('accounts', 'uid', ['username' => $username]);
return new self($uid * 1);
}
return new self(-1, $username);
}

/**
* Add a user to the system. /!\ Assumes input is OK /!\
* @param string $username Username, saved in lowercase.
* @param string $password Password, will be hashed before saving.
* @param string $realname User's real legal name
* @param string $email User's email address.
* @param string $phone1 Phone number #1
* @param string $phone2 Phone number #2
* @param int $type Account type
* @return int The new user's ID number in the database.
*/
public static function add(string $username, string $password, string $realname, string $email = null, string $phone1 = "", string $phone2 = "", int $type = 1): int {
global $database;
$database->insert('accounts', [
'username' => strtolower($username),
'password' => (is_null($password) ? null : encryptPassword($password)),
'realname' => $realname,
'email' => $email,
'phone1' => $phone1,
'phone2' => $phone2,
'acctstatus' => 1,
'accttype' => $type
]);
return $database->id();
}

public function exists(): bool {
return $this->exists;
}

public function has2fa(): bool {
return $this->has2fa;
}

function getUsername() {
return $this->username;
}

function getUID() {
return $this->uid;
}

function getEmail() {
return $this->email;
}

function getName() {
return $this->realname;
}

/**
* Check the given plaintext password against the stored hash.
* @param string $password
* @return bool
*/
function checkPassword(string $password): bool {
return password_verify($password, $this->passhash);
}

/**
* Change the user's password.
* @global $database $database
* @param string $old The current password
* @param string $new The new password
* @param string $new2 New password again
* @throws PasswordMatchException
* @throws PasswordMismatchException
* @throws IncorrectPasswordException