RestService.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. using System.Reflection;
  2. using InABox.Clients;
  3. using InABox.Core;
  4. using InABox.Database;
  5. namespace InABox.API
  6. {
  7. public class RestService
  8. {
  9. public static bool CheckPasswordExpiration { get; set; } = true;
  10. protected static void GarbageCollection()
  11. {
  12. //DateTime now = DateTime.Now;
  13. //GC.Collect();
  14. //GC.WaitForPendingFinalizers();
  15. //GC.Collect();
  16. //Logger.Send(LogType.Information, "", String.Format("Garbage Collection takes {0:F2}ms", (DateTime.Now - now).TotalMilliseconds));
  17. }
  18. private static IQueryDef GetQuery<T>(string filterString, string columnsString, string sortString)
  19. where T : Entity, IRemotable, IPersistent, new()
  20. {
  21. var filter = Core.Serialization.Deserialize<Filter<T>>(filterString);
  22. var columns = Core.Serialization.Deserialize<Columns<T>>(columnsString);
  23. var sort = Core.Serialization.Deserialize<SortOrder<T>>(sortString);
  24. return new QueryDef<T>(filter, columns, sort);
  25. }
  26. protected static Guid ValidateRequest(Request request, out string? userID)
  27. {
  28. var session = request.Credentials.Session;
  29. if (session != Guid.Empty)
  30. {
  31. var valid = CredentialsCache.Validate(session, out userID);
  32. if (valid != Guid.Empty)
  33. {
  34. CredentialsCache.RefreshSessionExpiry(session);
  35. }
  36. return valid;
  37. }
  38. else if (request.Credentials.UserID is not null
  39. && request.Credentials.Password is not null
  40. && CredentialsCache.IsBypassed(request.Credentials.UserID, request.Credentials.Password))
  41. {
  42. userID = "";
  43. return CoreUtils.FullGuid;
  44. }
  45. userID = null;
  46. return Guid.Empty;
  47. }
  48. public static MultiQueryResponse QueryMultiple(MultiQueryRequest request, bool isSecure)
  49. {
  50. var start = DateTime.Now;
  51. var response = new MultiQueryResponse();
  52. var userguid = ValidateRequest(request, out var userid);
  53. if (userguid == Guid.Empty)
  54. return response;
  55. Logger.Send(LogType.Information, userid, string.Format("[{0} {1}] QueryMultiple({2})",
  56. request.Credentials.Platform,
  57. request.Credentials.Version,
  58. request.TableTypes.Count));
  59. try
  60. {
  61. var getQueryMethod = typeof(RestService).GetMethod(nameof(RestService.GetQuery), BindingFlags.NonPublic | BindingFlags.Static);
  62. var queries = new Dictionary<string, IQueryDef>();
  63. foreach (var item in request.TableTypes)
  64. {
  65. var type = CoreUtils.GetEntity(item.Value);
  66. if (type.IsAssignableTo(typeof(ISecure)) && !isSecure)
  67. {
  68. Logger.Send(LogType.Error, userid, $"{type} is a secure entity. Request failed");
  69. }
  70. else
  71. {
  72. queries.Add(item.Key, getQueryMethod.MakeGenericMethod(type).Invoke(null, new object[]
  73. {
  74. request.Filters[item.Key],
  75. request.Columns[item.Key],
  76. request.Sorts[item.Key]
  77. }) as IQueryDef);
  78. }
  79. }
  80. response.Tables = DbFactory.QueryMultiple(queries, userguid, userid, request.Credentials.Platform, request.Credentials.Version);
  81. response.Status = StatusCode.OK;
  82. Logger.Send(LogType.Information, userid,
  83. string.Format("[{0} {1}] [{2:D8}] QueryMultiple Complete", request.Credentials.Platform, request.Credentials.Version,
  84. (int)DateTime.Now.Subtract(start).TotalMilliseconds));
  85. }
  86. catch (Exception err)
  87. {
  88. response.Status = StatusCode.Error;
  89. response.Messages.Add(err.Message);
  90. Logger.Send(LogType.Error, userid,
  91. string.Format("[{0} {1}] [{2:D8}] QueryMultiple Failed: {3} ", request.Credentials.Platform, request.Credentials.Version,
  92. (int)DateTime.Now.Subtract(start).TotalMilliseconds, err.Message));
  93. }
  94. GarbageCollection();
  95. return response;
  96. }
  97. public static ValidateResponse Validate(ValidateRequest request)
  98. {
  99. var response = new ValidateResponse();
  100. User? user = null;
  101. bool reLogin = false;
  102. if (request.Credentials.Session != Guid.Empty)
  103. {
  104. var session = request.Credentials.Session;
  105. user = CredentialsCache.Validate(session);
  106. if (user != null)
  107. {
  108. Logger.Send(LogType.Information, "", $"{session} re-logged in!");
  109. CredentialsCache.RefreshSessionExpiry(session);
  110. reLogin = true;
  111. }
  112. }
  113. if (user == null)
  114. {
  115. if (request.UsePIN)
  116. {
  117. Logger.Send(LogType.Information, "", $"Login request with PIN {request.PIN}");
  118. user = CredentialsCache.ValidateUser(request.PIN);
  119. }
  120. else
  121. {
  122. var userID = request.UserID ?? request.Credentials.UserID;
  123. var password = request.Password ?? request.Credentials.Password;
  124. if (userID == null || password == null)
  125. return response.Status(StatusCode.Error);
  126. user = CredentialsCache.ValidateUser(userID, password);
  127. if (user?.ID != CoreUtils.FullGuid)
  128. {
  129. Logger.Send(LogType.Information, userID, $"Login request for {userID}");
  130. }
  131. }
  132. }
  133. response.Status = StatusCode.OK;
  134. if (user == null)
  135. {
  136. Logger.Send(LogType.Information, "", $"Login failed!");
  137. response.ValidationResult = ValidationResult.INVALID;
  138. }
  139. else if (CheckPasswordExpiration && !request.UsePIN && user.PasswordExpiration > DateTime.MinValue && user.PasswordExpiration < DateTime.Now)
  140. {
  141. Logger.Send(LogType.Information, user.UserID, $"Password for ({user.UserID}) has expired!");
  142. response.ValidationResult = ValidationResult.PASSWORD_EXPIRED;
  143. }
  144. else if (reLogin)
  145. {
  146. Logger.Send(LogType.Information, user.UserID, $"Login ({user.UserID}) success!");
  147. response.ValidationResult = ValidationResult.VALID;
  148. response.UserGuid = user.ID;
  149. response.UserID = user.UserID;
  150. response.SecurityID = user.SecurityGroup.ID;
  151. response.Session = request.Credentials.Session;
  152. response.PasswordExpiration = CheckPasswordExpiration ? user.PasswordExpiration : DateTime.MinValue;
  153. }
  154. else if (user.ID == CoreUtils.FullGuid || !user.Use2FA)
  155. {
  156. Logger.Send(LogType.Information, user.UserID, $"Login ({user.UserID}) success!");
  157. response.ValidationResult = ValidationResult.VALID;
  158. response.UserGuid = user.ID;
  159. response.UserID = user.UserID;
  160. response.SecurityID = user.SecurityGroup.ID;
  161. response.Session = user.ID == CoreUtils.FullGuid ?
  162. CredentialsCache.NewSession(user, true, DateTime.MaxValue) :
  163. CredentialsCache.NewSession(user, true);
  164. response.PasswordExpiration = CheckPasswordExpiration ? user.PasswordExpiration : DateTime.MinValue;
  165. }
  166. else
  167. {
  168. Logger.Send(LogType.Information, user.UserID, $"Login ({user.UserID}) requires 2FA. Sending code...");
  169. var session = CredentialsCache.SendCode(user.ID, out var recipient);
  170. if (session != null)
  171. {
  172. response.ValidationResult = ValidationResult.REQUIRE_2FA;
  173. response.UserGuid = user.ID;
  174. response.UserID = user.UserID;
  175. response.SecurityID = user.SecurityGroup.ID;
  176. response.Session = session ?? Guid.Empty;
  177. response.Recipient2FA = recipient;
  178. response.PasswordExpiration = CheckPasswordExpiration ? user.PasswordExpiration : DateTime.MinValue;
  179. }
  180. else
  181. {
  182. response.Status = StatusCode.Error;
  183. response.Messages.Add("Code failed to send");
  184. }
  185. }
  186. return response;
  187. }
  188. public static Check2FAResponse Check2FA(Check2FARequest request)
  189. {
  190. var response = new Check2FAResponse();
  191. Logger.Send(LogType.Information, "", $"2FA check for session {request.Credentials.Session} and code {request.Code}");
  192. response.Valid = CredentialsCache.ValidateCode(request.Credentials.Session, request.Code);
  193. response.Status = StatusCode.OK;
  194. return response;
  195. }
  196. public static InfoResponse Info(InfoRequest request)
  197. {
  198. var response = new InfoResponse()
  199. {
  200. Info = new DatabaseInfo()
  201. {
  202. ColorScheme = DbFactory.ColorScheme,
  203. Version = CoreUtils.GetVersion(),
  204. Logo = DbFactory.Logo
  205. }
  206. };
  207. response.Status = StatusCode.OK;
  208. return response;
  209. }
  210. }
  211. public class RestService<TEntity> : RestService where TEntity : Entity, new()
  212. {
  213. private static string SimpleName(Type t)
  214. {
  215. var names = t.Name.Split('.');
  216. return names[names.Length - 1];
  217. }
  218. public static QueryResponse<TEntity> List(QueryRequest<TEntity> request)
  219. {
  220. var start = DateTime.Now;
  221. var response = new QueryResponse<TEntity>();
  222. var userguid = ValidateRequest(request, out var userid);
  223. if (userguid == Guid.Empty)
  224. return response.Status(StatusCode.Unauthenticated);
  225. Logger.Send(LogType.Information, userid,
  226. string.Format("[{0} {1}] Query{2}: Filter=[{3}] Sort=[{4}]", request.Credentials.Platform, request.Credentials.Version,
  227. SimpleName(typeof(TEntity)), request.Filter != null ? request.Filter.AsOData() : "",
  228. request.Sort != null ? request.Sort.AsOData() : ""));
  229. try
  230. {
  231. var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version);
  232. response.Items = store.Query(request.Filter, request.Columns, request.Sort);
  233. response.Status = StatusCode.OK;
  234. Logger.Send(LogType.Information, userid,
  235. string.Format("[{0} {1}] [{2:D8}] Query{3} Complete: {4} records / {5} columns returned", request.Credentials.Platform,
  236. request.Credentials.Version, (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)),
  237. response.Items.Rows.Count, response.Items.Columns.Count));
  238. }
  239. catch (Exception err)
  240. {
  241. response.Status = StatusCode.Error;
  242. response.Messages.Add(err.Message);
  243. Logger.Send(LogType.Error, userid,
  244. string.Format("[{0} {1}] [{2:D8}] Query{3} Failed: {4} ", request.Credentials.Platform, request.Credentials.Version,
  245. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message));
  246. }
  247. GarbageCollection();
  248. return response;
  249. }
  250. /*public static LoadResponse<TEntity> Load(LoadRequest<TEntity> request)
  251. {
  252. var start = DateTime.Now;
  253. var response = new LoadResponse<TEntity>();
  254. var userguid = ValidateRequest(request, out var userid);
  255. if (userguid == Guid.Empty)
  256. return response.Status(StatusCode.Unauthenticated);
  257. Logger.Send(LogType.Information, userid,
  258. string.Format("[{0} {1}] Load{2}: Filter=[{3}]", request.Credentials.Platform, request.Credentials.Version,
  259. SimpleName(typeof(TEntity)),
  260. request.Filter != null ? request.Filter.AsOData() : ""));
  261. try
  262. {
  263. var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version);
  264. response.Items = store.Load(request.Filter, request.Sort);
  265. response.Status = StatusCode.OK;
  266. Logger.Send(LogType.Information, userid,
  267. string.Format("[{0} {1}] [{2:D8}] Load{3} Complete: {4} records returned", request.Credentials.Platform,
  268. request.Credentials.Version,
  269. (uint)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), response.Items.Count()));
  270. }
  271. catch (Exception err)
  272. {
  273. response.Status = StatusCode.Error;
  274. response.Messages.Add(err.Message);
  275. Logger.Send(LogType.Error, userid,
  276. string.Format("[{0} {1}] [{2:D8}] Load{3} Failed: {4} ", request.Credentials.Platform, request.Credentials.Version,
  277. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message));
  278. }
  279. GarbageCollection();
  280. return response;
  281. }*/
  282. public static SaveResponse<TEntity> Save(SaveRequest<TEntity> request)
  283. {
  284. var start = DateTime.Now;
  285. var response = new SaveResponse<TEntity>();
  286. var userguid = ValidateRequest(request, out var userid);
  287. if (userguid == Guid.Empty)
  288. return response.Status(StatusCode.Unauthenticated);
  289. Logger.Send(LogType.Information, userid,
  290. string.Format("[{0} {1}] Save{2}: Data=[{3}]", request.Credentials.Platform, request.Credentials.Version, SimpleName(typeof(TEntity)),
  291. request.Item != null ? request.Item.ToString() : request + " (null)"));
  292. try
  293. {
  294. var e = request.Item;
  295. var preSaveChanges = BaseObjectExtensions.GetOriginaValues(e, false);
  296. var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version);
  297. store.Save(e, request.AuditNote);
  298. if (request.ReturnOnlyChanged)
  299. {
  300. var currentChanges = BaseObjectExtensions.GetOriginaValues(e, false);
  301. foreach (var (key, value) in currentChanges)
  302. {
  303. if (preSaveChanges.TryGetValue(key, out var oldValue))
  304. {
  305. if (!Equals(value, oldValue))
  306. {
  307. response.ChangedValues.Add(key, CoreUtils.GetPropertyValue(e, key));
  308. }
  309. }
  310. else
  311. {
  312. response.ChangedValues.Add(key, CoreUtils.GetPropertyValue(e, key));
  313. }
  314. }
  315. }
  316. else
  317. {
  318. response.Item = e;
  319. }
  320. response.Status = StatusCode.OK;
  321. Logger.Send(LogType.Information, userid,
  322. string.Format("[{0} {1}] [{2:D8}] Save{3} Data=[{4}] Complete", request.Credentials.Platform, request.Credentials.Version,
  323. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), request.Item));
  324. CredentialsCache.Refresh(typeof(TEntity) == typeof(User));
  325. }
  326. catch (Exception err)
  327. {
  328. response.Status = StatusCode.Error;
  329. response.Messages.Add(err.Message);
  330. Logger.Send(LogType.Error, userid,
  331. string.Format("[{0} {1}] [{2:D8}] Save{3} Failed: {4}\n\n{5} ", request.Credentials.Platform, request.Credentials.Version,
  332. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message, err.StackTrace));
  333. }
  334. GarbageCollection();
  335. return response;
  336. }
  337. public static MultiSaveResponse<TEntity> MultiSave(MultiSaveRequest<TEntity> request)
  338. {
  339. var start = DateTime.Now;
  340. var response = new MultiSaveResponse<TEntity>();
  341. var userguid = ValidateRequest(request, out var userid);
  342. if (userguid == Guid.Empty)
  343. return response.Status(StatusCode.Unauthenticated);
  344. Logger.Send(LogType.Information, userid,
  345. string.Format("[{0} {1}] MultiSave{2}({3}) Data=[{4}]", request.Credentials.Platform, request.Credentials.Version,
  346. SimpleName(typeof(TEntity)), request.Items.Length,
  347. request.Items != null ? string.Join(", ", request.Items.Select(x => x.ToString())) : request + " (null)"));
  348. try
  349. {
  350. var originals = new Dictionary<TEntity, Dictionary<string, object>>();
  351. var es = request.Items;
  352. foreach (var e in es)
  353. originals[e] = BaseObjectExtensions.GetOriginaValues(e, false);
  354. var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version);
  355. store.Save(es, request.AuditNote);
  356. if (request.ReturnOnlyChanged)
  357. {
  358. for (int i = 0; i < es.Length; ++i)
  359. {
  360. var result = new Dictionary<string, object>();
  361. var e = es[i];
  362. var currentChanges = BaseObjectExtensions.GetOriginaValues(e, false);
  363. var preSaveChanges = originals[e];
  364. foreach (var (key, value) in currentChanges)
  365. {
  366. if (preSaveChanges.TryGetValue(key, out var oldValue))
  367. {
  368. if (!Equals(value, oldValue))
  369. {
  370. result[key] = CoreUtils.GetPropertyValue(e, key);
  371. }
  372. }
  373. else
  374. {
  375. result[key] = CoreUtils.GetPropertyValue(e, key);
  376. }
  377. }
  378. response.ChangedValues.Add(result);
  379. }
  380. }
  381. else
  382. {
  383. response.Items = es;
  384. }
  385. response.Status = StatusCode.OK;
  386. Logger.Send(LogType.Information, userid,
  387. string.Format("[{0} {1}] [{2:D8}] MultiSave{3} Count=[{4}] Complete", request.Credentials.Platform, request.Credentials.Version,
  388. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), es.Length));
  389. CredentialsCache.Refresh(typeof(TEntity) == typeof(User));
  390. }
  391. catch (Exception err)
  392. {
  393. response.Status = StatusCode.Error;
  394. response.Messages.Add(err.Message);
  395. Logger.Send(LogType.Error, userid,
  396. string.Format("[{0} {1}] [{2:D8}] MultiSave{3} Failed: {4}\n\n{5} ", request.Credentials.Platform, request.Credentials.Version,
  397. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message, err.StackTrace));
  398. }
  399. GarbageCollection();
  400. return response;
  401. }
  402. public static DeleteResponse<TEntity> Delete(DeleteRequest<TEntity> request)
  403. {
  404. var start = DateTime.Now;
  405. var response = new DeleteResponse<TEntity>();
  406. var userguid = ValidateRequest(request, out var userid);
  407. if (userguid == Guid.Empty)
  408. return response.Status(StatusCode.Unauthenticated);
  409. Logger.Send(LogType.Information, userid,
  410. string.Format("[{0} {1}] Delete{2} Data=[{3}]", request.Credentials.Platform, request.Credentials.Version,
  411. SimpleName(typeof(TEntity)),
  412. request.Item));
  413. try
  414. {
  415. var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version);
  416. store.Delete(request.Item, request.AuditNote);
  417. response.Status = StatusCode.OK;
  418. Logger.Send(LogType.Information, userid,
  419. string.Format("[{0} {1}] [{2:D8}] Delete{3} Complete", request.Credentials.Platform, request.Credentials.Version,
  420. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity))));
  421. CredentialsCache.Refresh(typeof(TEntity) == typeof(User));
  422. }
  423. catch (Exception err)
  424. {
  425. response.Status = StatusCode.Error;
  426. response.Messages.Add(err.Message);
  427. Logger.Send(LogType.Error, userid,
  428. string.Format("[{0} {1}] [{2:D8}] Delete{3} Failed: {4} ", request.Credentials.Platform, request.Credentials.Version,
  429. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message));
  430. }
  431. GarbageCollection();
  432. return response;
  433. }
  434. public static MultiDeleteResponse<TEntity> MultiDelete(MultiDeleteRequest<TEntity> request)
  435. {
  436. var start = DateTime.Now;
  437. var response = new MultiDeleteResponse<TEntity>();
  438. var userguid = ValidateRequest(request, out var userid);
  439. if (userguid == Guid.Empty)
  440. return response.Status(StatusCode.Unauthenticated);
  441. Logger.Send(LogType.Information, userid,
  442. string.Format("[{0} {1}] MultiDelete{2}({3}) Data=[{4}]", request.Credentials.Platform, request.Credentials.Version,
  443. SimpleName(typeof(TEntity)), request.Items.Length,
  444. request.Items != null ? string.Join(", ", request.Items.Select(x => x.ToString())) : request + " (null)"));
  445. try
  446. {
  447. var es = request.Items;
  448. var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version);
  449. store.Delete(es, request.AuditNote);
  450. response.Status = StatusCode.OK;
  451. Logger.Send(LogType.Information, userid,
  452. string.Format("[{0} {1}] [{2:D8}] MultiDelete{3} Count=[{4}] Complete", request.Credentials.Platform, request.Credentials.Version,
  453. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), request.Items.Length));
  454. CredentialsCache.Refresh(typeof(TEntity) == typeof(User));
  455. }
  456. catch (Exception err)
  457. {
  458. response.Status = StatusCode.Error;
  459. response.Messages.Add(err.Message);
  460. Logger.Send(LogType.Error, userid,
  461. string.Format("[{0} {1}] [{2:D8}] MultiDelete{3} Failed: {4}\n\n{5} ", request.Credentials.Platform, request.Credentials.Version,
  462. (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message, err.StackTrace));
  463. }
  464. GarbageCollection();
  465. return response;
  466. }
  467. }
  468. }