diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 06552962..da73e5e3 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -20,6 +20,7 @@ + @@ -50,7 +51,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - all runtime; build; native; contentfiles; analyzers @@ -59,7 +59,6 @@ all runtime; build; native; contentfiles; analyzers - @@ -103,6 +102,7 @@ + diff --git a/src/MZikmund.sln b/src/MZikmund.sln index c6855a74..e84e2aa5 100644 --- a/src/MZikmund.sln +++ b/src/MZikmund.sln @@ -42,8 +42,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MZikmund.Skia.Gtk", "app\MZ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MZikmund.Skia.Linux.FrameBuffer", "app\MZikmund.Skia.Linux.FrameBuffer\MZikmund.Skia.Linux.FrameBuffer.csproj", "{0EBB04BE-4BB8-4F44-AA52-04B1967FCCEC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MZikmund.Skia.WPF", "app\MZikmund.Skia.WPF\MZikmund.Skia.WPF.csproj", "{CAD5D4B9-FB38-4997-A9A8-10B89282518E}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MZikmund.Wasm", "app\MZikmund.Wasm\MZikmund.Wasm.csproj", "{DBCC4BA1-A25D-4ECA-B599-CE5B14E74B67}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MZikmund.Windows", "app\MZikmund.Windows\MZikmund.Windows.csproj", "{6A207857-8DAD-47D3-AAEB-CEF69C468841}" @@ -760,62 +758,6 @@ Global {0EBB04BE-4BB8-4F44-AA52-04B1967FCCEC}.Release|x64.Build.0 = Release|Any CPU {0EBB04BE-4BB8-4F44-AA52-04B1967FCCEC}.Release|x86.ActiveCfg = Release|Any CPU {0EBB04BE-4BB8-4F44-AA52-04B1967FCCEC}.Release|x86.Build.0 = Release|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Ad-Hoc|ARM64.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Ad-Hoc|ARM64.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Ad-Hoc|x64.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Ad-Hoc|x86.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.AppStore|ARM.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.AppStore|ARM.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.AppStore|ARM64.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.AppStore|ARM64.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.AppStore|iPhone.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.AppStore|x64.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.AppStore|x64.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.AppStore|x86.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.AppStore|x86.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Debug|ARM.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Debug|ARM.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Debug|ARM64.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Debug|ARM64.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Debug|iPhone.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Debug|x64.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Debug|x64.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Debug|x86.ActiveCfg = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Debug|x86.Build.0 = Debug|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Release|Any CPU.Build.0 = Release|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Release|ARM.ActiveCfg = Release|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Release|ARM.Build.0 = Release|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Release|ARM64.ActiveCfg = Release|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Release|ARM64.Build.0 = Release|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Release|iPhone.ActiveCfg = Release|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Release|iPhone.Build.0 = Release|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Release|x64.ActiveCfg = Release|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Release|x64.Build.0 = Release|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Release|x86.ActiveCfg = Release|Any CPU - {CAD5D4B9-FB38-4997-A9A8-10B89282518E}.Release|x86.Build.0 = Release|Any CPU {DBCC4BA1-A25D-4ECA-B599-CE5B14E74B67}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {DBCC4BA1-A25D-4ECA-B599-CE5B14E74B67}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {DBCC4BA1-A25D-4ECA-B599-CE5B14E74B67}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU @@ -1114,7 +1056,6 @@ Global {0D99E167-C1F3-4996-875B-9E9EB969B722} = {143CCBE2-8344-4BC0-9812-FA6B3EA703EA} {BA5F6CFD-EEF6-467A-A99B-E5001DB1259F} = {143CCBE2-8344-4BC0-9812-FA6B3EA703EA} {0EBB04BE-4BB8-4F44-AA52-04B1967FCCEC} = {143CCBE2-8344-4BC0-9812-FA6B3EA703EA} - {CAD5D4B9-FB38-4997-A9A8-10B89282518E} = {143CCBE2-8344-4BC0-9812-FA6B3EA703EA} {DBCC4BA1-A25D-4ECA-B599-CE5B14E74B67} = {143CCBE2-8344-4BC0-9812-FA6B3EA703EA} {6A207857-8DAD-47D3-AAEB-CEF69C468841} = {143CCBE2-8344-4BC0-9812-FA6B3EA703EA} {C2E30D34-5B71-432F-B5D5-69B88B5AF5DB} = {6AD11812-0C83-402E-83D6-6D46B2CB7AF5} diff --git a/src/app/MZikmund.Windows/MZikmund.Windows.csproj b/src/app/MZikmund.Windows/MZikmund.Windows.csproj index 260aa299..02d82be9 100644 --- a/src/app/MZikmund.Windows/MZikmund.Windows.csproj +++ b/src/app/MZikmund.Windows/MZikmund.Windows.csproj @@ -1,4 +1,4 @@ - + WinExe net8.0-windows10.0.19041.0 @@ -11,7 +11,7 @@ true - true + diff --git a/src/app/MZikmund/App.cs b/src/app/MZikmund/App.cs index 94d98ffb..a84a6cfc 100644 --- a/src/app/MZikmund/App.cs +++ b/src/app/MZikmund/App.cs @@ -131,6 +131,7 @@ private static void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddSingleton(); services.AddSingleton(provider => { var configuration = provider.GetRequiredService>(); @@ -144,7 +145,7 @@ async Task GetTokenAsync(HttpRequestMessage message, CancellationToken c { //TODO: Move somewhere more appropriate and integrate refresh token support var userService = Ioc.Default.GetRequiredService(); - if (!userService.IsLoggedIn) + if (!userService.IsLoggedIn || userService.NeedsRefresh) { await userService.AuthenticateAsync(); } diff --git a/src/app/MZikmund/MZikmund.csproj b/src/app/MZikmund/MZikmund.csproj index 05253616..950f97cc 100644 --- a/src/app/MZikmund/MZikmund.csproj +++ b/src/app/MZikmund/MZikmund.csproj @@ -10,10 +10,12 @@ + all + @@ -27,6 +29,7 @@ + diff --git a/src/app/MZikmund/Services/Account/AuthenticationConstants.cs b/src/app/MZikmund/Services/Account/AuthenticationConstants.cs new file mode 100644 index 00000000..61ad64a6 --- /dev/null +++ b/src/app/MZikmund/Services/Account/AuthenticationConstants.cs @@ -0,0 +1,16 @@ +namespace MZikmund.Services.Account; + +public static class AuthenticationConstants +{ + public const string ApplicationId = "7e13557a-4799-46b8-9e2b-0f31c41a051e"; + + public const string TenantId = "4e973842-1a98-40ec-9542-3c2019f0fb8e"; + + public static string[] DefaultScopes { get; } = new string[] + { + "api://862d5839-f30f-41a9-ab6f-ff7eef19342c/access_as_user", + "user.read", + "profile", + "offline_access" + }; +} diff --git a/src/app/MZikmund/Services/Account/AuthenticationInfo.cs b/src/app/MZikmund/Services/Account/AuthenticationInfo.cs new file mode 100644 index 00000000..d566d278 --- /dev/null +++ b/src/app/MZikmund/Services/Account/AuthenticationInfo.cs @@ -0,0 +1,12 @@ +namespace MZikmund.Services.Account; + +public class AuthenticationInfo +{ + public required string DisplayName { get; init; } + + public required DateTimeOffset ExpiresOn { get; init; } + + public required string Token { get; init; } + + public required string UserId { get; init; } +} diff --git a/src/app/MZikmund/Services/Account/IUserService.cs b/src/app/MZikmund/Services/Account/IUserService.cs index 2282fc3a..17d47ae0 100644 --- a/src/app/MZikmund/Services/Account/IUserService.cs +++ b/src/app/MZikmund/Services/Account/IUserService.cs @@ -4,6 +4,10 @@ public interface IUserService { bool IsLoggedIn { get; } + bool NeedsRefresh { get; } + + string? UserName { get; } + string? AccessToken { get; } Task AuthenticateAsync(); diff --git a/src/app/MZikmund/Services/Account/UserService.cs b/src/app/MZikmund/Services/Account/UserService.cs new file mode 100644 index 00000000..30aa2fa1 --- /dev/null +++ b/src/app/MZikmund/Services/Account/UserService.cs @@ -0,0 +1,129 @@ +using Microsoft.Identity.Client; +using Uno.UI.MSAL; +using MZikmund.Services.Preferences; +using Microsoft.Identity.Client.Extensions.Msal; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace MZikmund.Services.Account; + +public class UserService : IUserService +{ + private IPublicClientApplication? _identityClient; + private AuthenticationInfo? _authenticationInfo; + + public UserService() + { + } + + public bool IsLoggedIn => _authenticationInfo != null; + + public string? UserName => _authenticationInfo?.DisplayName; + + public bool NeedsRefresh => _authenticationInfo?.ExpiresOn < DateTimeOffset.UtcNow.AddMinutes(-5); + + public string? AccessToken => _authenticationInfo?.Token; + + public async Task AuthenticateAsync() + { + if (IsLoggedIn && !NeedsRefresh) + { + return; + } + + await EnsureIdentityClientAsync(); + + var accounts = await _identityClient.GetAccountsAsync(); + AuthenticationResult? result = null; + bool tryInteractiveLogin = false; + + try + { + result = await _identityClient + .AcquireTokenSilent(AuthenticationConstants.DefaultScopes, accounts.FirstOrDefault()) + .ExecuteAsync(); + } + catch (MsalUiRequiredException) + { + tryInteractiveLogin = true; + } + catch (Exception ex) + { + Debug.WriteLine($"MSAL Silent Error: {ex.Message}"); + } + + if (tryInteractiveLogin) + { + try + { + result = await _identityClient + .AcquireTokenInteractive(AuthenticationConstants.DefaultScopes) + .ExecuteAsync(); + } + catch (Exception ex) + { + Debug.WriteLine($"MSAL Interactive Error: {ex.Message}"); + } + } + + _authenticationInfo = new AuthenticationInfo + { + DisplayName = result?.Account?.Username ?? "", + ExpiresOn = result?.ExpiresOn ?? DateTimeOffset.MinValue, + Token = result?.AccessToken ?? "", + UserId = result?.Account?.Username ?? "" + }; + } + + [MemberNotNull(nameof(_identityClient))] + private async Task EnsureIdentityClientAsync() + { + if (_identityClient == null) + { +#if __ANDROID__ + _identityClient = PublicClientApplicationBuilder + .Create(AuthenticationConstants.ApplicationId) + .WithAuthority(AzureCloudInstance.AzurePublic, AuthenticationConstants.TenantId) + .WithRedirectUri($"msal{AuthenticationConstants.ApplicationId}://auth") + .WithParentActivityOrWindow(() => ContextHelper.Current) + .Build(); + + await Task.CompletedTask; +#elif __IOS__ + _identityClient = PublicClientApplicationBuilder + .Create(AuthenticationConstants.ApplicationId) + .WithAuthority(AzureCloudInstance.AzurePublic, AuthenticationConstants.TenantId) + .WithIosKeychainSecurityGroup("com.microsoft.adalcache") + .WithRedirectUri($"msal{AuthenticationConstants.ApplicationId}://auth") + .Build(); + + await Task.CompletedTask; +#else + _identityClient = PublicClientApplicationBuilder + .Create(AuthenticationConstants.ApplicationId) + .WithAuthority(AzureCloudInstance.AzurePublic, AuthenticationConstants.TenantId) + .WithRedirectUri("https://login.microsoftonline.com/common/oauth2/nativeclient") + .WithUnoHelpers() + .Build(); + + await AttachTokenCacheAsync(); +#endif + } + } + +#if !__ANDROID__ && !__IOS__ + private async Task AttachTokenCacheAsync() + { +#if !HAS_UNO + // Cache configuration and hook-up to public application. Refer to https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet/wiki/Cross-platform-Token-Cache#configuring-the-token-cache + var storageProperties = new StorageCreationPropertiesBuilder("msal.cache", ApplicationData.Current.LocalFolder.Path) + .Build(); + + var msalcachehelper = await MsalCacheHelper.CreateAsync(storageProperties); + msalcachehelper.RegisterCache(_identityClient!.UserTokenCache); +#else + await Task.CompletedTask; +#endif + } +#endif +} diff --git a/src/app/MZikmund/ViewModels/Admin/BlogCategoriesManagerViewModel.cs b/src/app/MZikmund/ViewModels/Admin/BlogCategoriesManagerViewModel.cs index 5035137c..0e927957 100644 --- a/src/app/MZikmund/ViewModels/Admin/BlogCategoriesManagerViewModel.cs +++ b/src/app/MZikmund/ViewModels/Admin/BlogCategoriesManagerViewModel.cs @@ -32,12 +32,18 @@ public CategoriesManagerViewModel( public ObservableCollection Categories { get; } = new ObservableCollection(); public override async void ViewAppeared() + { + await RefreshListAsync(); + } + + private async Task RefreshListAsync() { using var loadingScope = _loadingIndicator.BeginLoading(); try { //TODO: Refresh collection based on IDs var categories = await _api.GetCategoriesAsync(); + Categories.Clear(); Categories.AddRange(categories.Content!); } catch (Exception ex) @@ -51,33 +57,7 @@ await _dialogService.ShowStatusMessageAsync( public ICommand AddCategoryCommand => GetOrCreateAsyncCommand(AddCategoryAsync); - public ICommand UpdateCategoryCommand => GetOrCreateAsyncCommand(UpdateCategoryAsync); - - public ICommand ImportJsonCommand => GetOrCreateAsyncCommand(ImportJsonAsync); - - private async Task ImportJsonAsync() - { - using var loadingScope = _loadingIndicator.BeginLoading(); - var picker = new FileOpenPicker(); - picker.FileTypeFilter.Add(".json"); - var jsonFile = await picker.PickSingleFileAsync(); - var jsonContent = await FileIO.ReadTextAsync(jsonFile); - var Categories = JsonConvert.DeserializeObject(jsonContent); - if (Categories == null) - { - return; - } - - for (int i = 0; i < Categories.Length; i++) - { - var tag = Categories[i]; - _loadingIndicator.StatusMessage = $"Adding tag {i + 1} of {Categories.Length}"; - // Ensure tag ID is empty. - tag.Id = Guid.Empty; - - await _api.AddCategoryAsync(tag); - } - } + public ICommand UpdateCategoryCommand => GetOrCreateAsyncCommand(UpdateCategoryAsync); private async Task AddCategoryAsync() { @@ -88,34 +68,42 @@ private async Task AddCategoryAsync() return; } - //var apiResponse = await _api.AddCategoryAsync(new CategoryDto() - //{ - // Localizations = new[] - // { - // new CategoryLocalizationDto() - // { - // DisplayName = "test", - // LanguageId = 1, - // RouteName = "test" - // } - // } - //}); + var apiResponse = await _api.AddCategoryAsync(new Category() + { + DisplayName = viewModel.Category.DisplayName, + RouteName = viewModel.Category.RouteName, + }); + + await RefreshListAsync(); } - private async Task UpdateCategoryAsync(CategoryViewModel? category) + private async Task UpdateCategoryAsync(Category? category) { if (category is null) { return; } - var viewModel = new AddOrUpdateCategoryDialogViewModel(category); + var viewModel = new AddOrUpdateCategoryDialogViewModel(new CategoryViewModel() + { + Id = category.Id, + DisplayName = category.DisplayName, + RouteName = category.RouteName + }); var result = await _dialogService.ShowAsync(viewModel); - if (result != ContentDialogResult.Primary) + if (result == ContentDialogResult.Primary) { - return; + var apiResponse = await _api.UpdateCategoryAsync(category.Id, new EditCategory() + { + DisplayName = viewModel.Category.DisplayName, + RouteName = viewModel.Category.RouteName, + }); + } + else if (result == ContentDialogResult.Secondary) + { + var apiResponse = await _api.DeleteCategoryAsync(category.Id); } - // TODO: Update category via API. + await RefreshListAsync(); } } diff --git a/src/app/MZikmund/ViewModels/Admin/BlogCategoryViewModel.cs b/src/app/MZikmund/ViewModels/Admin/BlogCategoryViewModel.cs index 79bc4ac0..ae97de50 100644 --- a/src/app/MZikmund/ViewModels/Admin/BlogCategoryViewModel.cs +++ b/src/app/MZikmund/ViewModels/Admin/BlogCategoryViewModel.cs @@ -4,7 +4,7 @@ namespace MZikmund.ViewModels.Admin; public class CategoryViewModel : ObservableObject { - public int Id { get; set; } + public Guid Id { get; set; } public string DisplayName { get; set; } = ""; diff --git a/src/app/MZikmund/ViewModels/Admin/BlogTagViewModel.cs b/src/app/MZikmund/ViewModels/Admin/BlogTagViewModel.cs index 11101d30..316e35a5 100644 --- a/src/app/MZikmund/ViewModels/Admin/BlogTagViewModel.cs +++ b/src/app/MZikmund/ViewModels/Admin/BlogTagViewModel.cs @@ -4,7 +4,7 @@ namespace MZikmund.ViewModels.Admin; public class TagViewModel : ObservableObject { - public int Id { get; set; } + public Guid Id { get; set; } public string DisplayName { get; set; } = ""; diff --git a/src/app/MZikmund/ViewModels/Admin/BlogTagsManagerViewModel.cs b/src/app/MZikmund/ViewModels/Admin/BlogTagsManagerViewModel.cs index 5ab7bc12..33145bf3 100644 --- a/src/app/MZikmund/ViewModels/Admin/BlogTagsManagerViewModel.cs +++ b/src/app/MZikmund/ViewModels/Admin/BlogTagsManagerViewModel.cs @@ -1,13 +1,13 @@ using System.Collections.ObjectModel; using MZikmund.Api.Client; +using MZikmund.DataContracts.Blog; using MZikmund.Extensions; using MZikmund.Models.Dialogs; using MZikmund.Services.Dialogs; using MZikmund.Services.Loading; +using MZikmund.Services.Localization; using Newtonsoft.Json; using Windows.Storage.Pickers; -using MZikmund.Services.Localization; -using MZikmund.DataContracts.Blog; namespace MZikmund.ViewModels.Admin; @@ -32,12 +32,18 @@ public TagsManagerViewModel( public ObservableCollection Tags { get; } = new ObservableCollection(); public override async void ViewAppeared() + { + await RefreshListAsync(); + } + + private async Task RefreshListAsync() { using var loadingScope = _loadingIndicator.BeginLoading(); try { //TODO: Refresh collection based on IDs var tags = await _api.GetTagsAsync(); + Tags.Clear(); Tags.AddRange(tags.Content!); } catch (Exception ex) @@ -51,79 +57,53 @@ await _dialogService.ShowStatusMessageAsync( public ICommand AddTagCommand => GetOrCreateAsyncCommand(AddTagAsync); - public ICommand ImportJsonCommand => GetOrCreateAsyncCommand(ImportJsonAsync); + public ICommand UpdateTagCommand => GetOrCreateAsyncCommand(UpdateTagAsync); - private async Task ImportJsonAsync() + private async Task AddTagAsync() { - using var loadingScope = _loadingIndicator.BeginLoading(); - var picker = new FileOpenPicker(); - picker.FileTypeFilter.Add(".json"); - var jsonFile = await picker.PickSingleFileAsync(); - var jsonContent = await FileIO.ReadTextAsync(jsonFile); - var tags = JsonConvert.DeserializeObject(jsonContent); - if (tags == null) + var viewModel = new AddOrUpdateTagDialogViewModel(); + var result = await _dialogService.ShowAsync(viewModel); + if (result != ContentDialogResult.Primary) { return; } - for (int i = 0; i < tags.Length; i++) + var apiResponse = await _api.AddTagAsync(new Tag() { - var tag = tags[i]; - _loadingIndicator.StatusMessage = $"Adding tag {i + 1} of {tags.Length}"; - // Ensure tag ID is empty. - tag.Id = Guid.Empty; + DisplayName = viewModel.Tag.DisplayName, + RouteName = viewModel.Tag.RouteName, + }); - await _api.AddTagAsync(tag); - } + await RefreshListAsync(); } - private async Task AddTagAsync() + private async Task UpdateTagAsync(Tag? tag) { - var viewModel = new AddOrUpdateTagDialogViewModel(); - var result = await _dialogService.ShowAsync(viewModel); - if (result != ContentDialogResult.Primary) + if (tag is null) { return; } - //var apiResponse = await _api.AddTagAsync(new TagDto() - //{ - // Localizations = new[] - // { - // new TagLocalizationDto() - // { - // DisplayName = "test", - // LanguageId = 1, - // RouteName = "test" - // } - // } - //}); - } - - public async Task UpdateTagAsync(TagViewModel tag) - { - var viewModel = new AddOrUpdateTagDialogViewModel(tag); + var viewModel = new AddOrUpdateTagDialogViewModel(new TagViewModel() + { + Id = tag.Id, + DisplayName = tag.DisplayName, + RouteName = tag.RouteName + }); var result = await _dialogService.ShowAsync(viewModel); - if (result != ContentDialogResult.Primary) + if (result == ContentDialogResult.Primary) { - return; + var apiResponse = await _api.UpdateTagAsync(tag.Id, new EditTag() + { + DisplayName = viewModel.Tag.DisplayName, + RouteName = viewModel.Tag.RouteName, + }); + } + else if (result == ContentDialogResult.Secondary) + { + var apiResponse = await _api.DeleteTagAsync(tag.Id); } - await Task.CompletedTask; - - // TODO: Update tag via API - - //var apiResponse = await _api.AddTagAsync(new TagDto() - //{ - // Localizations = new[] - // { - // new TagLocalizationDto() - // { - // DisplayName = "test", - // LanguageId = 1, - // RouteName = "test" - // } - // } - //}); + await RefreshListAsync(); } } diff --git a/src/app/MZikmund/Views/Admin/BlogCategoriesManagerView.xaml b/src/app/MZikmund/Views/Admin/BlogCategoriesManagerView.xaml index 3b676145..59ccdb10 100644 --- a/src/app/MZikmund/Views/Admin/BlogCategoriesManagerView.xaml +++ b/src/app/MZikmund/Views/Admin/BlogCategoriesManagerView.xaml @@ -2,49 +2,64 @@ x:Class="MZikmund.Views.Admin.CategoriesManagerView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:local="using:MZikmund.Views.Admin" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:dto="using:MZikmund.DataContracts.Blog" + xmlns:local="using:MZikmund.Views.Admin" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:xaml="using:MZikmund.Extensions.Xaml" - xmlns:dto="using:MZikmund.DataContracts.Blog" - mc:Ignorable="d" - d:Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> + d:Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" + mc:Ignorable="d"> - + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/MZikmund/Views/Admin/BlogCategoriesManagerView.xaml.cs b/src/app/MZikmund/Views/Admin/BlogCategoriesManagerView.xaml.cs index 0ff0d93d..bdbdea5e 100644 --- a/src/app/MZikmund/Views/Admin/BlogCategoriesManagerView.xaml.cs +++ b/src/app/MZikmund/Views/Admin/BlogCategoriesManagerView.xaml.cs @@ -1,10 +1,17 @@ -using MZikmund.ViewModels.Admin; +using MZikmund.DataContracts.Blog; +using MZikmund.ViewModels.Admin; namespace MZikmund.Views.Admin; public sealed partial class CategoriesManagerView : CategoriesManagerViewBase { public CategoriesManagerView() => InitializeComponent(); + + private void GridView_ItemClick(object sender, ItemClickEventArgs e) + { + var category = (Category)e.ClickedItem; + ViewModel!.UpdateCategoryCommand.Execute(category); + } } public partial class CategoriesManagerViewBase : PageBase diff --git a/src/app/MZikmund/Views/Admin/BlogTagsManagerView.xaml b/src/app/MZikmund/Views/Admin/BlogTagsManagerView.xaml index e10925de..7c01bcf1 100644 --- a/src/app/MZikmund/Views/Admin/BlogTagsManagerView.xaml +++ b/src/app/MZikmund/Views/Admin/BlogTagsManagerView.xaml @@ -2,49 +2,64 @@ x:Class="MZikmund.Views.Admin.TagsManagerView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:local="using:MZikmund.Views.Admin" - xmlns:dto="using:MZikmund.DataContracts.Blog" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:dto="using:MZikmund.DataContracts.Blog" + xmlns:local="using:MZikmund.Views.Admin" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:xaml="using:MZikmund.Extensions.Xaml" - mc:Ignorable="d" - d:Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> + d:Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" + mc:Ignorable="d"> - + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/src/app/MZikmund/Views/Admin/BlogTagsManagerView.xaml.cs b/src/app/MZikmund/Views/Admin/BlogTagsManagerView.xaml.cs index 03a3d720..90494274 100644 --- a/src/app/MZikmund/Views/Admin/BlogTagsManagerView.xaml.cs +++ b/src/app/MZikmund/Views/Admin/BlogTagsManagerView.xaml.cs @@ -1,10 +1,17 @@ -using MZikmund.ViewModels.Admin; +using MZikmund.DataContracts.Blog; +using MZikmund.ViewModels.Admin; namespace MZikmund.Views.Admin; public sealed partial class TagsManagerView : TagsManagerViewBase { public TagsManagerView() => InitializeComponent(); + + private void GridView_ItemClick(object sender, ItemClickEventArgs e) + { + var tag = (Tag)e.ClickedItem; + ViewModel!.UpdateTagCommand.Execute(tag); + } } public partial class TagsManagerViewBase : PageBase diff --git a/src/app/MZikmund/appsettings.json b/src/app/MZikmund/appsettings.json index 1cc2e596..18073438 100644 --- a/src/app/MZikmund/appsettings.json +++ b/src/app/MZikmund/appsettings.json @@ -1,4 +1,8 @@ { + "Msal": { + "ClientId": "7e13557a-4799-46b8-9e2b-0f31c41a051e", + "Scopes": [ "User.Read" ] + }, "AppConfig": { "Environment": "Production", "ApiUrl": "https://mzikmund.dev/api" diff --git a/src/shared/MZikmund.Api/IMZikmundApi.Tags.cs b/src/shared/MZikmund.Api/IMZikmundApi.Tags.cs index 85ba5db4..d521a399 100644 --- a/src/shared/MZikmund.Api/IMZikmundApi.Tags.cs +++ b/src/shared/MZikmund.Api/IMZikmundApi.Tags.cs @@ -14,9 +14,9 @@ public partial interface IMZikmundApi [Put("/v1/admin/tags/{tagId}")] [Headers("Authorization: Bearer")] - Task> UpdateTagAsync(int tagId, [Body] EditTag tag); + Task> UpdateTagAsync(Guid tagId, [Body] EditTag tag); [Delete("/v1/admin/tags/{tagId}")] [Headers("Authorization: Bearer")] - Task> DeleteTagAsync(int tagId); + Task> DeleteTagAsync(Guid tagId); } diff --git a/src/web/MZikmund.Web.Core/Mappings/CategoryMap.cs b/src/web/MZikmund.Web.Core/Mappings/CategoryMap.cs index 4ac35901..30a43d7f 100644 --- a/src/web/MZikmund.Web.Core/Mappings/CategoryMap.cs +++ b/src/web/MZikmund.Web.Core/Mappings/CategoryMap.cs @@ -9,5 +9,6 @@ public class CatgegoryMap : Profile public CatgegoryMap() { CreateMap(); + CreateMap(); } } diff --git a/src/web/MZikmund.Web.Core/Mappings/TagMap.cs b/src/web/MZikmund.Web.Core/Mappings/TagMap.cs index d709434d..9c8459d9 100644 --- a/src/web/MZikmund.Web.Core/Mappings/TagMap.cs +++ b/src/web/MZikmund.Web.Core/Mappings/TagMap.cs @@ -9,5 +9,6 @@ public class TagMap : Profile public TagMap() { CreateMap(); + CreateMap(); } } diff --git a/src/web/MZikmund.Web/Controllers/TestController.cs b/src/web/MZikmund.Web/Controllers/TestController.cs new file mode 100644 index 00000000..f1e3c7b2 --- /dev/null +++ b/src/web/MZikmund.Web/Controllers/TestController.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.Resource; + +namespace MZikmund.Web.Controllers; + +[ApiController] +[Authorize] +[Route("api/v1/test")] +public class TestController +{ + [HttpGet] + [Route("")] + public IActionResult Get() => new OkObjectResult("Hello world!"); +} diff --git a/src/web/MZikmund.Web/Program.cs b/src/web/MZikmund.Web/Program.cs index 6abccedd..a2bc7935 100644 --- a/src/web/MZikmund.Web/Program.cs +++ b/src/web/MZikmund.Web/Program.cs @@ -44,6 +44,7 @@ } else { + app.UseDeveloperExceptionPage(); app.UseSwagger(); app.UseSwaggerUI(); } @@ -56,10 +57,11 @@ app.UseRouting(); app.UseCors(); +app.MapHealthChecks("/health"); +app.UseAuthentication(); app.UseAuthorization(); -app.MapHealthChecks("/health"); app.MapControllers(); app.MapRazorPages(); @@ -99,7 +101,7 @@ void ConfigureServices(IServiceCollection services, IConfiguration configuration services.AddLocalization(options => options.ResourcesPath = "Resources"); - services.AddControllers(options => options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute())) + services.AddControllers() .AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); services.AddEndpointsApiExplorer(); diff --git a/src/web/MZikmund.Web/appsettings.Development.json b/src/web/MZikmund.Web/appsettings.Development.json index 0f4306ba..d12eda52 100644 --- a/src/web/MZikmund.Web/appsettings.Development.json +++ b/src/web/MZikmund.Web/appsettings.Development.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "DatabaseConnection": "Server=(localdb)\\MSSQLLocalDB;Database=MZikmundDb;Trusted_Connection=True;" + "DatabaseConnection": "Server=(localdb)\\MSSQLLocalDB;Database=MZikmundDb" }, "DetailedErrors": true, "Logging": {