RestService.cs 24 KB

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