UserViewModel.cs 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. using Avalonia.Threading;
  2. using CommunityToolkit.Mvvm.Input;
  3. using PixiEditor.Extensions.Common.Localization;
  4. using PixiEditor.Models.Commands.Attributes.Commands;
  5. using PixiEditor.OperatingSystem;
  6. using PixiEditor.PixiAuth;
  7. using PixiEditor.PixiAuth.Exceptions;
  8. namespace PixiEditor.ViewModels.SubViewModels;
  9. internal class UserViewModel : SubViewModel<ViewModelMain>
  10. {
  11. private LocalizedString? lastError = null;
  12. public PixiAuthClient PixiAuthClient { get; }
  13. public User? User { get; private set; }
  14. public bool NotLoggedIn => User?.SessionId is null || User.SessionId == Guid.Empty;
  15. public bool WaitingForActivation => User is { SessionId: not null } && string.IsNullOrEmpty(User.SessionToken);
  16. public bool IsLoggedIn => User is { SessionId: not null } && !string.IsNullOrEmpty(User.SessionToken);
  17. public AsyncRelayCommand<string> RequestLoginCommand { get; }
  18. public AsyncRelayCommand TryValidateSessionCommand { get; }
  19. public AsyncRelayCommand ResendActivationCommand { get; }
  20. public AsyncRelayCommand LogoutCommand { get; }
  21. public LocalizedString? LastError
  22. {
  23. get => lastError;
  24. set => SetProperty(ref lastError, value);
  25. }
  26. private bool apiValid = true;
  27. public DateTime? TimeToEndTimeout { get; private set; } = null;
  28. public string TimeToEndTimeoutString
  29. {
  30. get
  31. {
  32. if (TimeToEndTimeout == null)
  33. {
  34. return string.Empty;
  35. }
  36. TimeSpan timeLeft = TimeToEndTimeout.Value - DateTime.Now;
  37. return timeLeft.TotalSeconds > 0 ? $"({timeLeft:ss})" : string.Empty;
  38. }
  39. }
  40. public UserViewModel(ViewModelMain owner) : base(owner)
  41. {
  42. RequestLoginCommand = new AsyncRelayCommand<string>(RequestLogin);
  43. TryValidateSessionCommand = new AsyncRelayCommand(TryValidateSession);
  44. ResendActivationCommand = new AsyncRelayCommand(ResendActivation, CanResendActivation);
  45. LogoutCommand = new AsyncRelayCommand(Logout);
  46. string baseUrl = BuildConstants.PixiEditorApiUrl;
  47. #if DEBUG
  48. if (baseUrl.Contains('{') && baseUrl.Contains('}'))
  49. {
  50. string? envUrl = Environment.GetEnvironmentVariable("PIXIEDITOR_API_URL");
  51. if (envUrl != null)
  52. {
  53. baseUrl = envUrl;
  54. }
  55. }
  56. #endif
  57. try
  58. {
  59. PixiAuthClient = new PixiAuthClient(baseUrl);
  60. }
  61. catch (UriFormatException e)
  62. {
  63. Console.WriteLine($"Invalid api URL format: {e.Message}");
  64. apiValid = false;
  65. }
  66. Task.Run(async () =>
  67. {
  68. await LoadUserData();
  69. await TryRefreshToken();
  70. });
  71. }
  72. public async Task RequestLogin(string email)
  73. {
  74. if (!apiValid) return;
  75. try
  76. {
  77. Guid? session = await PixiAuthClient.GenerateSession(email);
  78. if (session != null)
  79. {
  80. LastError = null;
  81. User = new User(email) { SessionId = session.Value };
  82. NotifyProperties();
  83. SaveUserInfo();
  84. }
  85. }
  86. catch (PixiAuthException authException)
  87. {
  88. LastError = new LocalizedString(authException.Message);
  89. }
  90. }
  91. public async Task ResendActivation()
  92. {
  93. if (!apiValid) return;
  94. if (User?.SessionId == null)
  95. {
  96. return;
  97. }
  98. try
  99. {
  100. await PixiAuthClient.ResendActivation(User.Email, User.SessionId.Value);
  101. TimeToEndTimeout = DateTime.Now.Add(TimeSpan.FromSeconds(60));
  102. RunTimeoutTimers(60);
  103. NotifyProperties();
  104. LastError = null;
  105. }
  106. catch (TooManyRequestsException e)
  107. {
  108. LastError = new LocalizedString(e.Message, e.TimeLeft);
  109. TimeToEndTimeout = DateTime.Now.Add(TimeSpan.FromSeconds(e.TimeLeft));
  110. RunTimeoutTimers(e.TimeLeft);
  111. NotifyProperties();
  112. }
  113. catch (PixiAuthException authException)
  114. {
  115. LastError = new LocalizedString(authException.Message);
  116. }
  117. }
  118. private void RunTimeoutTimers(double timeLeft)
  119. {
  120. DispatcherTimer.RunOnce(
  121. () =>
  122. {
  123. TimeToEndTimeout = null;
  124. NotifyProperties();
  125. },
  126. TimeSpan.FromSeconds(timeLeft));
  127. DispatcherTimer.Run(() =>
  128. {
  129. NotifyProperties();
  130. return TimeToEndTimeout != null;
  131. }, TimeSpan.FromSeconds(1));
  132. }
  133. public bool CanResendActivation()
  134. {
  135. return WaitingForActivation && TimeToEndTimeout == null;
  136. }
  137. public async Task<bool> TryRefreshToken()
  138. {
  139. if (!apiValid) return false;
  140. if (!IsLoggedIn)
  141. {
  142. return false;
  143. }
  144. try
  145. {
  146. string? token = await PixiAuthClient.RefreshToken(User.SessionId.Value, User.SessionToken);
  147. if (token != null)
  148. {
  149. User.SessionToken = token;
  150. NotifyProperties();
  151. SaveUserInfo();
  152. return true;
  153. }
  154. }
  155. catch (ForbiddenException e)
  156. {
  157. User = null;
  158. NotifyProperties();
  159. SaveUserInfo();
  160. LastError = new LocalizedString(e.Message);
  161. }
  162. catch (PixiAuthException authException)
  163. {
  164. LastError = new LocalizedString(authException.Message);
  165. }
  166. return false;
  167. }
  168. public async Task<bool> TryValidateSession()
  169. {
  170. if (!apiValid) return false;
  171. if (User?.SessionId == null)
  172. {
  173. return false;
  174. }
  175. try
  176. {
  177. string? token = await PixiAuthClient.TryClaimSessionToken(User.Email, User.SessionId.Value);
  178. if (token != null)
  179. {
  180. LastError = null;
  181. User.SessionToken = token;
  182. NotifyProperties();
  183. SaveUserInfo();
  184. return true;
  185. }
  186. }
  187. catch (BadRequestException ex)
  188. {
  189. if (ex.Message == "SESSION_NOT_VALIDATED")
  190. {
  191. LastError = null;
  192. }
  193. }
  194. catch (PixiAuthException authException)
  195. {
  196. LastError = new LocalizedString(authException.Message);
  197. }
  198. return false;
  199. }
  200. public async Task Logout()
  201. {
  202. if (!apiValid) return;
  203. if (!IsLoggedIn)
  204. {
  205. return;
  206. }
  207. User = null;
  208. NotifyProperties();
  209. SaveUserInfo();
  210. await PixiAuthClient.Logout(User.SessionId.Value, User.SessionToken);
  211. }
  212. public async Task SaveUserInfo()
  213. {
  214. await IOperatingSystem.Current.SecureStorage.SetValueAsync("UserData", User);
  215. }
  216. public async Task LoadUserData()
  217. {
  218. User = await IOperatingSystem.Current.SecureStorage.GetValueAsync<User>("UserData", null);
  219. }
  220. private void NotifyProperties()
  221. {
  222. OnPropertyChanged(nameof(User));
  223. OnPropertyChanged(nameof(NotLoggedIn));
  224. OnPropertyChanged(nameof(WaitingForActivation));
  225. OnPropertyChanged(nameof(IsLoggedIn));
  226. OnPropertyChanged(nameof(LastError));
  227. OnPropertyChanged(nameof(TimeToEndTimeout));
  228. OnPropertyChanged(nameof(TimeToEndTimeoutString));
  229. ResendActivationCommand.NotifyCanExecuteChanged();
  230. }
  231. }