From 552b02d57a778fdbcd31349f03637a7b1913c151 Mon Sep 17 00:00:00 2001 From: "Yuto Terada (indigo-san)" <66758394+indigo-san@users.noreply.github.com> Date: Fri, 19 Jan 2024 17:25:26 +0900 Subject: [PATCH] =?UTF-8?q?`AuthorizedUser.RefreshAsync`=E3=81=A7`user.jso?= =?UTF-8?q?n`=E3=81=8C=E5=A4=89=E6=9B=B4=E3=81=95=E3=82=8C=E3=81=A6?= =?UTF-8?q?=E3=81=84=E3=82=8B=E5=A0=B4=E5=90=88=E3=80=81=E8=87=AA=E5=8B=95?= =?UTF-8?q?=E3=81=A7=E5=86=8D=E8=AA=AD=E3=81=BF=E8=BE=BC=E3=81=BF=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F=20(#898)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Beutl.Api/BeutlApiApplication.cs | 82 +++++++++++-------- src/Beutl.Api/Objects/AuthorizedUser.cs | 47 +++++++++-- .../AccountSettingsPageViewModel.cs | 2 +- 3 files changed, 92 insertions(+), 39 deletions(-) diff --git a/src/Beutl.Api/BeutlApiApplication.cs b/src/Beutl.Api/BeutlApiApplication.cs index d29e7e3c5..804627125 100644 --- a/src/Beutl.Api/BeutlApiApplication.cs +++ b/src/Beutl.Api/BeutlApiApplication.cs @@ -104,13 +104,16 @@ private void Register(Func factory) _services.Add(typeof(T), new Lazy(() => factory())); } - public void SignOut() + public void SignOut(bool deleteFile = true) { _authorizedUser.Value = null; - string fileName = Path.Combine(Helper.AppRoot, "user.json"); - if (File.Exists(fileName)) + if (deleteFile) { - File.Delete(fileName); + string fileName = Path.Combine(Helper.AppRoot, "user.json"); + if (File.Exists(fileName)) + { + File.Delete(fileName); + } } } @@ -155,7 +158,7 @@ private async Task SignInExternalAsync(string provider, Cancella ProfileResponse profileResponse = await Users.Get2Async(cancellationToken); var profile = new Profile(profileResponse, this); - _authorizedUser.Value = new AuthorizedUser(profile, authResponse, this, _httpClient); + _authorizedUser.Value = new AuthorizedUser(profile, authResponse, this, _httpClient, DateTime.UtcNow); SaveUser(); activity?.AddEvent(new("Saved_User")); return _authorizedUser.Value; @@ -196,7 +199,7 @@ public async Task SignInAsync(CancellationToken cancellationToke ProfileResponse profileResponse = await Users.Get2Async(cancellationToken); var profile = new Profile(profileResponse, this); - _authorizedUser.Value = new AuthorizedUser(profile, authResponse, this, _httpClient); + _authorizedUser.Value = new AuthorizedUser(profile, authResponse, this, _httpClient, DateTime.UtcNow); SaveUser(); activity?.AddEvent(new("Saved_User")); return _authorizedUser.Value; @@ -231,6 +234,8 @@ public void SaveUser() using var writer = new Utf8JsonWriter(stream); obj.WriteTo(writer); } + + user._writeTime = File.GetLastWriteTimeUtc(fileName); } } @@ -240,39 +245,50 @@ public async Task RestoreUserAsync(Activity? activity) { activity?.AddEvent(new("Entered_AsyncLock")); - string fileName = Path.Combine(Helper.AppRoot, "user.json"); - if (File.Exists(fileName)) + AuthorizedUser? user = await ReadUserAsync(); + if (user != null) { - JsonNode? node; - using (StreamReader reader = File.OpenText(fileName)) - { - string json = await reader.ReadToEndAsync(); - node = JsonNode.Parse(json); - } + await user.RefreshAsync(); - if (node != null) + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", user.Token); + await user.Profile.RefreshAsync(); + _authorizedUser.Value = user; + SaveUser(); + } + } + } + + public async ValueTask ReadUserAsync() + { + string fileName = Path.Combine(Helper.AppRoot, "user.json"); + if (File.Exists(fileName)) + { + JsonNode? node = JsonNode.Parse(await File.ReadAllTextAsync(fileName)); + DateTime lastWriteTime = File.GetLastWriteTimeUtc(fileName); + + if (node != null) + { + ProfileResponse? profile = JsonSerializer.Deserialize(node["profile"]); + string? token = (string?)node["token"]; + string? refreshToken = (string?)node["refresh_token"]; + var expiration = (DateTimeOffset?)node["expiration"]; + + if (profile != null + && token != null + && refreshToken != null + && expiration.HasValue) { - ProfileResponse? profile = JsonSerializer.Deserialize(node["profile"]); - string? token = (string?)node["token"]; - string? refreshToken = (string?)node["refresh_token"]; - var expiration = (DateTimeOffset?)node["expiration"]; - - if (profile != null - && token != null - && refreshToken != null - && expiration.HasValue) - { - var user = new AuthorizedUser(new Profile(profile, this), new AuthResponse(expiration.Value, refreshToken, token), this, _httpClient); - await user.RefreshAsync(); - - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", user.Token); - await user.Profile.RefreshAsync(); - _authorizedUser.Value = user; - SaveUser(); - } + return new AuthorizedUser( + new Profile(profile, this), + new AuthResponse(expiration.Value, refreshToken, token), + this, + _httpClient, + lastWriteTime); } } } + + return null; } private static int GetRandomUnusedPort() diff --git a/src/Beutl.Api/Objects/AuthorizedUser.cs b/src/Beutl.Api/Objects/AuthorizedUser.cs index 499dd9524..d11daa885 100644 --- a/src/Beutl.Api/Objects/AuthorizedUser.cs +++ b/src/Beutl.Api/Objects/AuthorizedUser.cs @@ -1,17 +1,25 @@ using System.Diagnostics; using System.Net.Http.Headers; +using Beutl.Api.Services; + namespace Beutl.Api.Objects; -public class AuthorizedUser(Profile profile, AuthResponse response, BeutlApiApplication clients, HttpClient httpClient) +public class AuthorizedUser( + Profile profile, AuthResponse response, + BeutlApiApplication clients, HttpClient httpClient, DateTime writeTime) { + private AuthResponse _response = response; + // user.jsonに書き込まれた時間 + internal DateTime _writeTime = writeTime; + public Profile Profile { get; } = profile; - public string Token => response.Token; + public string Token => _response.Token; - public string RefreshToken => response.Refresh_token; + public string RefreshToken => _response.Refresh_token; - public DateTimeOffset Expiration => response.Expiration; + public DateTimeOffset Expiration => _response.Expiration; public bool IsExpired => Expiration < DateTimeOffset.UtcNow; @@ -21,12 +29,41 @@ public async ValueTask RefreshAsync(bool force = false) { using Activity? activity = clients.ActivitySource.StartActivity("AuthorizedUser.Refresh", ActivityKind.Client); + string fileName = Path.Combine(Helper.AppRoot, "user.json"); + if (File.Exists(fileName)) + { + DateTime lastWriteTime = File.GetLastWriteTimeUtc(fileName); + if (_writeTime < lastWriteTime) + { + AuthorizedUser? fileUser = await clients.ReadUserAsync(); + if (fileUser?.Profile?.Id == Profile.Id) + { + _response = fileUser._response; + _writeTime = lastWriteTime; + } + else if (fileUser != null) + { + clients.SignOut(false); + throw new BeutlApiException( + message: "The user may have been changed in another process.", + statusCode: 401, + response: "", + headers: new Dictionary>(), + result: new ApiErrorResponse( + documentation_url: "", + error_code: ApiErrorCode.Unknown, + message: "The user may have been changed in another process."), + innerException: null); + } + } + } + activity?.SetTag("force", force); activity?.SetTag("is_expired", IsExpired); if (force || IsExpired) { - response = await clients.Account.RefreshAsync(new RefeshTokenRequest(RefreshToken, Token)) + _response = await clients.Account.RefreshAsync(new RefeshTokenRequest(RefreshToken, Token)) .ConfigureAwait(false); activity?.AddEvent(new("Refreshed")); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token); diff --git a/src/Beutl/ViewModels/SettingsPages/AccountSettingsPageViewModel.cs b/src/Beutl/ViewModels/SettingsPages/AccountSettingsPageViewModel.cs index a1349266d..4510cbccf 100644 --- a/src/Beutl/ViewModels/SettingsPages/AccountSettingsPageViewModel.cs +++ b/src/Beutl/ViewModels/SettingsPages/AccountSettingsPageViewModel.cs @@ -62,7 +62,7 @@ public AccountSettingsPageViewModel(BeutlApiApplication clients) .DisposeWith(_disposables); SignOut = new ReactiveCommand(SignedIn); - SignOut.Subscribe(_clients.SignOut).DisposeWith(_disposables); + SignOut.Subscribe(() => _clients.SignOut()).DisposeWith(_disposables); OpenAccountSettings = new(); OpenAccountSettings.Subscribe(BeutlApiApplication.OpenAccountSettings);