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 9.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  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. * @return bool
  91. */
  92. function checkPassword(string $password): bool {
  93. return password_verify($password, $this->passhash);
  94. }
  95. /**
  96. * Change the user's password.
  97. * @global $database $database
  98. * @param string $old The current password
  99. * @param string $new The new password
  100. * @param string $new2 New password again
  101. * @throws PasswordMatchException
  102. * @throws PasswordMismatchException
  103. * @throws IncorrectPasswordException
  104. * @throws WeakPasswordException
  105. */
  106. function changePassword(string $old, string $new, string $new2) {
  107. global $database, $SETTINGS;
  108. if ($old == $new) {
  109. throw new PasswordMatchException();
  110. }
  111. if ($new != $new2) {
  112. throw new PasswordMismatchException();
  113. }
  114. if (!$this->checkPassword($old)) {
  115. throw new IncorrectPasswordException();
  116. }
  117. require_once __DIR__ . "/worst_passwords.php";
  118. $passrank = checkWorst500List($new);
  119. if ($passrank !== FALSE) {
  120. throw new WeakPasswordException();
  121. }
  122. if (strlen($new) < $SETTINGS['min_password_length']) {
  123. throw new WeakPasswordException();
  124. }
  125. $database->update('accounts', ['password' => password_hash($new, PASSWORD_DEFAULT), 'acctstatus' => 1], ['uid' => $this->uid]);
  126. Log::insert(LogType::PASSWORD_CHANGED, $this);
  127. return true;
  128. }
  129. function check2fa(string $code): bool {
  130. if (!$this->has2fa) {
  131. return true;
  132. }
  133. $totp = new TOTP(null, $this->authsecret);
  134. $time = time();
  135. if ($totp->verify($code, $time)) {
  136. return true;
  137. }
  138. if ($totp->verify($code, $time - 30)) {
  139. return true;
  140. }
  141. if ($totp->verify($code, $time + 30)) {
  142. return true;
  143. }
  144. return false;
  145. }
  146. /**
  147. * Generate a TOTP secret for the given user.
  148. * @return string OTP provisioning URI (for generating a QR code)
  149. */
  150. function generate2fa(): string {
  151. global $SETTINGS;
  152. $secret = random_bytes(20);
  153. $encoded_secret = Base32::encode($secret);
  154. $totp = new TOTP((empty($this->email) ? $this->realname : $this->email), $encoded_secret);
  155. $totp->setIssuer($SETTINGS['system_name']);
  156. return $totp->getProvisioningUri();
  157. }
  158. /**
  159. * Save a TOTP secret for the user.
  160. * @global $database $database
  161. * @param string $username
  162. * @param string $secret
  163. */
  164. function save2fa(string $secret) {
  165. global $database;
  166. $database->update('accounts', ['authsecret' => $secret], ['username' => $this->username]);
  167. }
  168. /**
  169. * Check if the given username has the given permission (or admin access)
  170. * @global $database $database
  171. * @param string $code
  172. * @return boolean TRUE if the user has the permission (or admin access), else FALSE
  173. */
  174. function hasPermission(string $code): bool {
  175. global $database;
  176. return $database->has('assigned_permissions', [
  177. '[>]permissions' => [
  178. 'permid' => 'permid'
  179. ]
  180. ], ['AND' => ['OR' => ['permcode #code' => $code, 'permcode #admin' => 'ADMIN'], 'uid' => $this->uid]]) === TRUE;
  181. }
  182. /**
  183. * Get the account status.
  184. * @return \AccountStatus
  185. */
  186. function getStatus(): AccountStatus {
  187. global $database;
  188. $statuscode = $database->get('accounts', 'acctstatus', ['uid' => $this->uid]);
  189. return new AccountStatus($statuscode);
  190. }
  191. function sendAlertEmail(string $appname = null) {
  192. global $SETTINGS;
  193. if (is_null($appname)) {
  194. $appname = $SETTINGS['site_title'];
  195. }
  196. if (empty(ADMIN_EMAIL) || filter_var(ADMIN_EMAIL, FILTER_VALIDATE_EMAIL) === FALSE) {
  197. return "invalid_to_email";
  198. }
  199. if (empty(FROM_EMAIL) || filter_var(FROM_EMAIL, FILTER_VALIDATE_EMAIL) === FALSE) {
  200. return "invalid_from_email";
  201. }
  202. $mail = new PHPMailer;
  203. if ($SETTINGS['debug']) {
  204. $mail->SMTPDebug = 2;
  205. }
  206. if ($SETTINGS['email']['use_smtp']) {
  207. $mail->isSMTP();
  208. $mail->Host = $SETTINGS['email']['host'];
  209. $mail->SMTPAuth = $SETTINGS['email']['auth'];
  210. $mail->Username = $SETTINGS['email']['user'];
  211. $mail->Password = $SETTINGS['email']['password'];
  212. $mail->SMTPSecure = $SETTINGS['email']['secure'];
  213. $mail->Port = $SETTINGS['email']['port'];
  214. if ($SETTINGS['email']['allow_invalid_certificate']) {
  215. $mail->SMTPOptions = array(
  216. 'ssl' => array(
  217. 'verify_peer' => false,
  218. 'verify_peer_name' => false,
  219. 'allow_self_signed' => true
  220. )
  221. );
  222. }
  223. }
  224. $mail->setFrom(FROM_EMAIL, 'Account Alerts');
  225. $mail->addAddress(ADMIN_EMAIL, "System Admin");
  226. $mail->isHTML(false);
  227. $mail->Subject = $Strings->get("admin alert email subject", false);
  228. $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);
  229. if (!$mail->send()) {
  230. return $mail->ErrorInfo;
  231. }
  232. return true;
  233. }
  234. }
  235. class AccountStatus {
  236. const NORMAL = 1;
  237. const LOCKED_OR_DISABLED = 2;
  238. const CHANGE_PASSWORD = 3;
  239. const TERMINATED = 4;
  240. const ALERT_ON_ACCESS = 5;
  241. private $status;
  242. public function __construct(int $status) {
  243. $this->status = $status;
  244. }
  245. /**
  246. * Get the account status/state as an integer.
  247. * @return int
  248. */
  249. public function get(): int {
  250. return $this->status;
  251. }
  252. /**
  253. * Get the account status/state as a string representation.
  254. * @return string
  255. */
  256. public function getString(): string {
  257. switch ($this->status) {
  258. case self::NORMAL:
  259. return "NORMAL";
  260. case self::LOCKED_OR_DISABLED:
  261. return "LOCKED_OR_DISABLED";
  262. case self::CHANGE_PASSWORD:
  263. return "CHANGE_PASSWORD";
  264. case self::TERMINATED:
  265. return "TERMINATED";
  266. case self::ALERT_ON_ACCESS:
  267. return "ALERT_ON_ACCESS";
  268. default:
  269. return "OTHER_" . $this->status;
  270. }
  271. }
  272. }