Account and permission manager and security log viewer. https://netsyms.biz/apps/managepanel
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

User.lib.php 10.0KB

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