RestService.cs 24 KB

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