Single-sign-on and self-serve account management. https://netsyms.biz/apps/accounthub
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.

443 lines
20KB

  1. <?php
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. /**
  6. * Simple JSON API to allow other apps to access accounts in this system.
  7. *
  8. * Requests can be sent via either GET or POST requests. POST is recommended
  9. * as it has a lower chance of being logged on the server, exposing unencrypted
  10. * user passwords.
  11. */
  12. require __DIR__ . '/required.php';
  13. header("Content-Type: application/json");
  14. if (empty($VARS['key'])) {
  15. die("\"403 Unauthorized\"");
  16. } else {
  17. $key = $VARS['key'];
  18. if ($database->has('apikeys', ['key' => $key]) !== TRUE) {
  19. engageRateLimit();
  20. http_response_code(403);
  21. Log::insert(LogType::API_BAD_KEY, null, "Key: " . $key);
  22. die("\"403 Unauthorized\"");
  23. }
  24. }
  25. /**
  26. * Get the API key with most of the characters replaced with *s.
  27. * @global string $key
  28. * @return string
  29. */
  30. function getCensoredKey() {
  31. global $key;
  32. $resp = $key;
  33. if (strlen($key) > 5) {
  34. for ($i = 2; $i < strlen($key) - 2; $i++) {
  35. $resp[$i] = "*";
  36. }
  37. }
  38. return $resp;
  39. }
  40. if (empty($VARS['action'])) {
  41. http_response_code(404);
  42. die(json_encode("No action specified."));
  43. }
  44. switch ($VARS['action']) {
  45. case "ping":
  46. exit(json_encode(["status" => "OK"]));
  47. break;
  48. case "auth":
  49. $user = User::byUsername($VARS['username']);
  50. if ($user->checkPassword($VARS['password'])) {
  51. Log::insert(LogType::API_AUTH_OK, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
  52. exit(json_encode(["status" => "OK", "msg" => $Strings->get("login successful", false)]));
  53. } else {
  54. Log::insert(LogType::API_AUTH_FAILED, $user->getUID(), "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
  55. if ($user->exists()) {
  56. switch ($user->getStatus()->get()) {
  57. case AccountStatus::LOCKED_OR_DISABLED:
  58. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account locked", false)]));
  59. case AccountStatus::TERMINATED:
  60. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account terminated", false)]));
  61. case AccountStatus::CHANGE_PASSWORD:
  62. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("password expired", false)]));
  63. case AccountStatus::NORMAL:
  64. break;
  65. default:
  66. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account state error", false)]));
  67. }
  68. }
  69. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
  70. }
  71. break;
  72. case "userinfo":
  73. if (!empty($VARS['username'])) {
  74. $user = User::byUsername($VARS['username']);
  75. } else if (!empty($VARS['uid']) && is_numeric($VARS['uid'])) {
  76. $user = new User($VARS['uid']);
  77. } else {
  78. http_response_code(400);
  79. die("\"400 Bad Request\"");
  80. }
  81. if ($user->exists()) {
  82. $data = $database->get("accounts", ["uid", "username", "realname (name)", "email", "phone" => ["phone1 (1)", "phone2 (2)"], 'pin'], ["uid" => $user->getUID()]);
  83. $data['pin'] = (is_null($data['pin']) || $data['pin'] == "" ? false : true);
  84. exit(json_encode(["status" => "OK", "data" => $data]));
  85. } else {
  86. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
  87. }
  88. break;
  89. case "userexists":
  90. if (!empty($VARS['uid']) && is_numeric($VARS['uid'])) {
  91. $user = new User($VARS['uid']);
  92. } else if (!empty($VARS['username'])) {
  93. $user = User::byUsername($VARS['username']);
  94. } else {
  95. http_response_code(400);
  96. die("\"400 Bad Request\"");
  97. }
  98. exit(json_encode(["status" => "OK", "exists" => $user->exists()]));
  99. break;
  100. case "hastotp":
  101. exit(json_encode(["status" => "OK", "otp" => User::byUsername($VARS['username'])->has2fa()]));
  102. break;
  103. case "verifytotp":
  104. $user = User::byUsername($VARS['username']);
  105. if ($user->check2fa($VARS['code'])) {
  106. exit(json_encode(["status" => "OK", "valid" => true]));
  107. } else {
  108. Log::insert(LogType::API_BAD_2FA, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
  109. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("2fa incorrect", false), "valid" => false]));
  110. }
  111. break;
  112. case "acctstatus":
  113. exit(json_encode(["status" => "OK", "account" => User::byUsername($VARS['username'])->getStatus()->getString()]));
  114. case "login":
  115. // simulate a login, checking account status and alerts
  116. engageRateLimit();
  117. $user = User::byUsername($VARS['username']);
  118. if ($user->checkPassword($VARS['password'])) {
  119. switch ($user->getStatus()->getString()) {
  120. case "LOCKED_OR_DISABLED":
  121. Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
  122. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account locked", false)]));
  123. case "TERMINATED":
  124. Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
  125. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account terminated", false)]));
  126. case "CHANGE_PASSWORD":
  127. Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
  128. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("password expired", false)]));
  129. case "NORMAL":
  130. Log::insert(LogType::API_LOGIN_OK, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
  131. exit(json_encode(["status" => "OK"]));
  132. case "ALERT_ON_ACCESS":
  133. $user->sendAlertEmail();
  134. Log::insert(LogType::API_LOGIN_OK, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
  135. exit(json_encode(["status" => "OK", "alert" => true]));
  136. default:
  137. Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
  138. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("account state error", false)]));
  139. }
  140. } else {
  141. Log::insert(LogType::API_LOGIN_FAILED, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
  142. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
  143. }
  144. break;
  145. case "ismanagerof":
  146. if ($VARS['uid'] == "1") {
  147. $manager = new User($VARS['manager']);
  148. $employee = new User($VARS['employee']);
  149. } else {
  150. $manager = User::byUsername($VARS['manager']);
  151. $employee = User::byUsername($VARS['employee']);
  152. }
  153. if (!$manager->exists()) {
  154. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false), "user" => $VARS['manager']]));
  155. }
  156. if (!$employee->exists()) {
  157. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false), "user" => $VARS['employee']]));
  158. }
  159. if ($database->has('managers', ['AND' => ['managerid' => $manager->getUID(), 'employeeid' => $employee->getUID()]])) {
  160. exit(json_encode(["status" => "OK", "managerof" => true]));
  161. } else {
  162. exit(json_encode(["status" => "OK", "managerof" => false]));
  163. }
  164. break;
  165. case "getmanaged":
  166. if (!empty($VARS['uid'])) {
  167. $manager = new User($VARS['uid']);
  168. } else if (!empty($VARS['username'])) {
  169. $manager = User::byUsername($VARS['username']);
  170. } else {
  171. http_response_code(400);
  172. die("\"400 Bad Request\"");
  173. }
  174. if (!$manager->exists()) {
  175. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
  176. }
  177. if ($VARS['get'] == "username") {
  178. $managed = $database->select('managers', ['[>]accounts' => ['employeeid' => 'uid']], 'username', ['managerid' => $manager->getUID()]);
  179. } else {
  180. $managed = $database->select('managers', 'employeeid', ['managerid' => $manager->getUID()]);
  181. }
  182. exit(json_encode(["status" => "OK", "employees" => $managed]));
  183. break;
  184. case "getmanagers":
  185. if (!empty($VARS['uid'])) {
  186. $emp = new User($VARS['uid']);
  187. } else if (!empty($VARS['username'])) {
  188. $emp = User::byUsername($VARS['username']);
  189. } else {
  190. http_response_code(400);
  191. die("\"400 Bad Request\"");
  192. }
  193. if (!$emp->exists()) {
  194. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
  195. }
  196. $managers = $database->select('managers', 'managerid', ['employeeid' => $emp->getUID()]);
  197. exit(json_encode(["status" => "OK", "managers" => $managers]));
  198. break;
  199. case "usersearch":
  200. if (empty($VARS['search']) || strlen($VARS['search']) < 3) {
  201. exit(json_encode(["status" => "OK", "result" => []]));
  202. }
  203. $data = $database->select('accounts', ['uid', 'username', 'realname (name)'], ["OR" => ['username[~]' => $VARS['search'], 'realname[~]' => $VARS['search']], "LIMIT" => 10]);
  204. exit(json_encode(["status" => "OK", "result" => $data]));
  205. break;
  206. case "permission":
  207. if (empty($VARS['code'])) {
  208. http_response_code(400);
  209. die("\"400 Bad Request\"");
  210. }
  211. $perm = $VARS['code'];
  212. if (!empty($VARS['uid'])) {
  213. $user = new User($VARS['uid']);
  214. } else if (!empty($VARS['username'])) {
  215. $user = User::byUsername($VARS['username']);
  216. } else {
  217. http_response_code(400);
  218. die("\"400 Bad Request\"");
  219. }
  220. if (!$user->exists()) {
  221. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
  222. }
  223. exit(json_encode(["status" => "OK", "has_permission" => $user->hasPermission($perm)]));
  224. break;
  225. case "mobileenabled":
  226. exit(json_encode(["status" => "OK", "mobile" => MOBILE_ENABLED]));
  227. case "mobilevalid":
  228. if (empty($VARS['username']) || empty($VARS['code'])) {
  229. http_response_code(400);
  230. die("\"400 Bad Request\"");
  231. }
  232. $code = strtoupper($VARS['code']);
  233. $user_key_valid = $database->has('mobile_codes', ['[>]accounts' => ['uid' => 'uid']], ["AND" => ['mobile_codes.code' => $code, 'accounts.username' => strtolower($VARS['username'])]]);
  234. exit(json_encode(["status" => "OK", "valid" => $user_key_valid]));
  235. case "alertemail":
  236. engageRateLimit();
  237. if (empty($VARS['username']) || !User::byUsername($VARS['username'])->exists()) {
  238. http_response_code(400);
  239. die("\"400 Bad Request\"");
  240. }
  241. $appname = "???";
  242. if (!empty($VARS['appname'])) {
  243. $appname = $VARS['appname'];
  244. }
  245. $result = User::byUsername($VARS['username'])->sendAlertEmail($appname);
  246. if ($result === TRUE) {
  247. exit(json_encode(["status" => "OK"]));
  248. }
  249. exit(json_encode(["status" => "ERROR", "msg" => $result]));
  250. case "codelogin":
  251. $database->delete("onetimekeys", ["expires[<]" => date("Y-m-d H:i:s")]); // cleanup
  252. if ($database->has("onetimekeys", ["key" => $VARS['code'], "expires[>]" => date("Y-m-d H:i:s")])) {
  253. $user = $database->get("onetimekeys", ["[>]accounts" => ["uid" => "uid"]], ["username", "realname", "accounts.uid"], ["key" => $VARS['code']]);
  254. exit(json_encode(["status" => "OK", "user" => $user]));
  255. } else {
  256. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("no such code or code expired", false)]));
  257. }
  258. case "listapps":
  259. $apps = EXTERNAL_APPS;
  260. // Format paths as absolute URLs
  261. foreach ($apps as $k => $v) {
  262. if (strpos($apps[$k]['url'], "http") === FALSE) {
  263. $apps[$k]['url'] = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . ($_SERVER['SERVER_PORT'] != 80 || $_SERVER['SERVER_PORT'] != 443 ? ":" . $_SERVER['SERVER_PORT'] : "") . $apps[$k]['url'];
  264. }
  265. }
  266. exit(json_encode(["status" => "OK", "apps" => $apps]));
  267. case "getusersbygroup":
  268. if ($VARS['gid']) {
  269. if ($database->has("groups", ['groupid' => $VARS['gid']])) {
  270. $groupid = $VARS['gid'];
  271. } else {
  272. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("group does not exist", false)]));
  273. }
  274. } else {
  275. http_response_code(400);
  276. die("\"400 Bad Request\"");
  277. }
  278. if ($VARS['get'] == "username") {
  279. $users = $database->select('assigned_groups', ['[>]accounts' => ['uid' => 'uid']], 'username', ['groupid' => $groupid, "ORDER" => "username"]);
  280. } else if ($VARS['get'] == "detail") {
  281. $users = $database->select('assigned_groups', ['[>]accounts' => ['uid' => 'uid']], ['username', 'realname (name)', 'accounts.uid', 'pin'], ['groupid' => $groupid, "ORDER" => "realname"]);
  282. for ($i = 0; $i < count($users); $i++) {
  283. if (is_null($users[$i]['pin']) || $users[$i]['pin'] == "") {
  284. $users[$i]['pin'] = false;
  285. } else {
  286. $users[$i]['pin'] = true;
  287. }
  288. }
  289. } else {
  290. $users = $database->select('assigned_groups', 'uid', ['groupid' => $groupid]);
  291. }
  292. exit(json_encode(["status" => "OK", "users" => $users]));
  293. break;
  294. case "getgroupsbyuser":
  295. if ($VARS['uid']) {
  296. if ($database->has("accounts", ['uid' => $VARS['uid']])) {
  297. $empid = $VARS['uid'];
  298. } else {
  299. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
  300. }
  301. } else if ($VARS['username']) {
  302. if ($database->has("accounts", ['username' => strtolower($VARS['username'])])) {
  303. $empid = $database->select('accounts', 'uid', ['username' => strtolower($VARS['username'])]);
  304. } else {
  305. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("user does not exist", false)]));
  306. }
  307. } else {
  308. http_response_code(400);
  309. die("\"400 Bad Request\"");
  310. }
  311. $groups = $database->select('assigned_groups', ["[>]groups" => ["groupid" => "groupid"]], ['groups.groupid (id)', 'groups.groupname (name)'], ['uid' => $empid]);
  312. exit(json_encode(["status" => "OK", "groups" => $groups]));
  313. break;
  314. case "getgroups":
  315. $groups = $database->select('groups', ['groupid (id)', 'groupname (name)']);
  316. exit(json_encode(["status" => "OK", "groups" => $groups]));
  317. break;
  318. case "groupsearch":
  319. if (empty($VARS['search']) || strlen($VARS['search']) < 2) {
  320. exit(json_encode(["status" => "OK", "result" => []]));
  321. }
  322. $data = $database->select('groups', ['groupid (id)', 'groupname (name)'], ['groupname[~]' => $VARS['search'], "LIMIT" => 10]);
  323. exit(json_encode(["status" => "OK", "result" => $data]));
  324. break;
  325. case "checkpin":
  326. $pin = "";
  327. if (empty($VARS['pin'])) {
  328. http_response_code(400);
  329. die("\"400 Bad Request\"");
  330. }
  331. if (!empty($VARS['username'])) {
  332. $user = User::byUsername($VARS['username']);
  333. } else if (!empty($VARS['uid'])) {
  334. $user = new User($VARS['uid']);
  335. } else {
  336. http_response_code(400);
  337. die("\"400 Bad Request\"");
  338. }
  339. if ($user->exists()) {
  340. $pin = $database->get("accounts", "pin", ["uid" => $user->getUID()]);
  341. } else {
  342. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
  343. }
  344. if (is_null($pin) || $pin == "") {
  345. exit(json_encode(["status" => "ERROR", "pinvalid" => false, "nopinset" => true]));
  346. }
  347. exit(json_encode(["status" => "OK", "pinvalid" => ($pin == $VARS['pin'])]));
  348. break;
  349. case "getnotifications":
  350. if (!empty($VARS['username'])) {
  351. $user = User::byUsername($VARS['username']);
  352. } else if (!empty($VARS['uid'])) {
  353. $user = new User($VARS['uid']);
  354. } else {
  355. http_response_code(400);
  356. die("\"400 Bad Request\"");
  357. }
  358. try {
  359. $notifications = Notifications::get($user);
  360. exit(json_encode(["status" => "OK", "notifications" => $notifications]));
  361. } catch (Exception $ex) {
  362. exit(json_encode(["status" => "ERROR", "msg" => $ex->getMessage()]));
  363. }
  364. break;
  365. case "readnotification":
  366. if (!empty($VARS['username'])) {
  367. $user = User::byUsername($VARS['username']);
  368. } else if (!empty($VARS['uid'])) {
  369. $user = new User($VARS['uid']);
  370. } else {
  371. http_response_code(400);
  372. die("\"400 Bad Request\"");
  373. }
  374. if (empty($VARS['id'])) {
  375. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("invalid parameters", false)]));
  376. }
  377. try {
  378. Notifications::read($user, $VARS['id']);
  379. exit(json_encode(["status" => "OK"]));
  380. } catch (Exception $ex) {
  381. exit(json_encode(["status" => "ERROR", "msg" => $ex->getMessage()]));
  382. }
  383. break;
  384. case "addnotification":
  385. if (!empty($VARS['username'])) {
  386. $user = User::byUsername($VARS['username']);
  387. } else if (!empty($VARS['uid'])) {
  388. $user = new User($VARS['uid']);
  389. } else {
  390. http_response_code(400);
  391. die("\"400 Bad Request\"");
  392. }
  393. try {
  394. $timestamp = "";
  395. if (!empty($VARS['timestamp'])) {
  396. $timestamp = date("Y-m-d H:i:s", strtotime($VARS['timestamp']));
  397. }
  398. $url = "";
  399. if (!empty($VARS['url'])) {
  400. $url = $VARS['url'];
  401. }
  402. $nid = Notifications::add($user, $VARS['title'], $VARS['content'], $timestamp, $url, isset($VARS['sensitive']));
  403. exit(json_encode(["status" => "OK", "id" => $nid]));
  404. } catch (Exception $ex) {
  405. exit(json_encode(["status" => "ERROR", "msg" => $ex->getMessage()]));
  406. }
  407. break;
  408. case "deletenotification":
  409. if (!empty($VARS['username'])) {
  410. $user = User::byUsername($VARS['username']);
  411. } else if (!empty($VARS['uid'])) {
  412. $user = new User($VARS['uid']);
  413. } else {
  414. http_response_code(400);
  415. die("\"400 Bad Request\"");
  416. }
  417. if (empty($VARS['id'])) {
  418. exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("invalid parameters", false)]));
  419. }
  420. try {
  421. Notifications::delete($user, $VARS['id']);
  422. exit(json_encode(["status" => "OK"]));
  423. } catch (Exception $ex) {
  424. exit(json_encode(["status" => "ERROR", "msg" => $ex->getMessage()]));
  425. }
  426. break;
  427. default:
  428. http_response_code(404);
  429. die(json_encode("404 Not Found: the requested action is not available."));
  430. }