From 8b3af89410eca06dbcb57ccefb814124ca8f2754 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Wed, 19 Aug 2026 20:30:34 +0100 Subject: [PATCH 001/214] access the post selector --- src/PostIt/PostIt/Views/MainPage.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PostIt/PostIt/Views/MainPage.axaml b/src/PostIt/PostIt/Views/MainPage.axaml index e30d1a846..7eac39ff4 100644 --- a/src/PostIt/PostIt/Views/MainPage.axaml +++ b/src/PostIt/PostIt/Views/MainPage.axaml @@ -67,7 +67,7 @@ + HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinHeight="40"> From 2e3ad2c99c73743ad54a4fa801e4c3e56935393e Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Wed, 19 Aug 2026 21:02:28 +0100 Subject: [PATCH 002/214] gixes the path to circles API --- src/Yavsc.Blogs/Controllers/BlogApiController.cs | 1 - src/Yavsc.Blogs/Controllers/CircleApiController.cs | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Yavsc.Blogs/Controllers/BlogApiController.cs b/src/Yavsc.Blogs/Controllers/BlogApiController.cs index 3901d9cbe..76aa777e6 100644 --- a/src/Yavsc.Blogs/Controllers/BlogApiController.cs +++ b/src/Yavsc.Blogs/Controllers/BlogApiController.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Yavsc.Blogspot; -using Yavsc.Models.Blog; using Yavsc.Server.Exceptions; using Yavsc.Server.Helpers; using static Yavsc.Blogs.Constants; diff --git a/src/Yavsc.Blogs/Controllers/CircleApiController.cs b/src/Yavsc.Blogs/Controllers/CircleApiController.cs index c35da7c58..45ee885a5 100644 --- a/src/Yavsc.Blogs/Controllers/CircleApiController.cs +++ b/src/Yavsc.Blogs/Controllers/CircleApiController.cs @@ -4,11 +4,12 @@ using Microsoft.EntityFrameworkCore; using Yavsc.Models; using Yavsc.Models.Relationship; using Yavsc.Server.Helpers; +using static Yavsc.Blogs.Constants; namespace Yavsc.Blogs.Controllers { [Produces("application/json")] - [Route("api/circle")] + [Route(APIPrefix +"/circle")] public class CircleApiController : Controller { private readonly ApplicationDbContext _context; From 7621ac86dbb633de1c705175f7d8a710d345d302 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Thu, 20 Aug 2026 01:23:14 +0100 Subject: [PATCH 003/214] fixes the cirle POST --- .../CircleMembersApiTests.cs | 2 +- .../Controllers/CircleApiController.cs | 60 ++++++++++++++++--- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/Yavsc.Blogs.Tests/CircleMembersApiTests.cs b/src/Yavsc.Blogs.Tests/CircleMembersApiTests.cs index 4e9bfb00f..5e8040ef4 100644 --- a/src/Yavsc.Blogs.Tests/CircleMembersApiTests.cs +++ b/src/Yavsc.Blogs.Tests/CircleMembersApiTests.cs @@ -88,7 +88,7 @@ public sealed class CircleMembersApiTests : IClassFixture } private string MembersUrl(long circleId) - => $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/api/circle/{circleId}/members"; + => $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{Constants.APIPrefix}/circle/{circleId}/members"; private HttpClient NewClient(string subject) { diff --git a/src/Yavsc.Blogs/Controllers/CircleApiController.cs b/src/Yavsc.Blogs/Controllers/CircleApiController.cs index 45ee885a5..73bbfff94 100644 --- a/src/Yavsc.Blogs/Controllers/CircleApiController.cs +++ b/src/Yavsc.Blogs/Controllers/CircleApiController.cs @@ -57,12 +57,25 @@ namespace Yavsc.Blogs.Controllers /// /// Replaces a circle. The caller must own it; the server - /// reasserts ownership regardless of any OwnerId the client - /// tries to put in the body. + /// reasserts ownership regardless of any OwnerId + /// the client tries to put in the body. + /// + /// The body shape is a — a + /// flat, navigation-free projection — not the EF entity. + /// The EF entity carries [JsonIgnore]-decorated + /// navigation properties (Owner, Members) + /// that bind to server-only types (ApplicationUser, + /// CircleMember); keeping the wire shape as a + /// DTO avoids any future regression where the entity + /// grows a navigable property that System.Text.Json + /// refuses to materialise. The client-side mirror lives + /// in Yavsc.Api.Client.Dtos.CircleDto. /// // PUT: api/circle/5 [HttpPut("{id}")] - public async Task PutCircle([FromRoute] long id, [FromBody] Circle circle) + public async Task PutCircle( + [FromRoute] long id, + [FromBody] CircleDto circle) { if (!ModelState.IsValid) { @@ -82,9 +95,14 @@ namespace Yavsc.Blogs.Controllers return new ChallengeResult(); } - // Force OwnerId to the caller; the body value is ignored. - circle.OwnerId = uid; - _context.Entry(circle).State = EntityState.Modified; + // Map the wire shape onto the entity. OwnerId is + // forced to the caller regardless of what the body + // says; Name and Public come from the body. + existing.Name = circle.Name; + existing.Public = circle.Public; + existing.OwnerId = uid; + + _context.Entry(existing).State = EntityState.Modified; try { @@ -111,7 +129,7 @@ namespace Yavsc.Blogs.Controllers /// // POST: api/circle [HttpPost] - public async Task PostCircle([FromBody] Circle circle) + public async Task PostCircle([FromBody] CircleDto circle) { if (!ModelState.IsValid) { @@ -120,8 +138,14 @@ namespace Yavsc.Blogs.Controllers var uid = User.GetUserId(); circle.OwnerId = uid; + Circle newCircle = new Circle + { + OwnerId = User.GetUserId(), + Name = circle.Name, + Public = circle.Public + }; - _context.Circle.Add(circle); + _context.Circle.Add(newCircle); try { await _context.SaveChangesAsync(User.GetUserId()); @@ -322,6 +346,26 @@ namespace Yavsc.Blogs.Controllers } } + /// + /// Wire shape for PUT /api/circle/{id}. Flat by + /// design — navigation properties (Owner, + /// Members) live on the EF entity only and never + /// cross the wire. + /// + /// Field names match the JSON the server emits + /// (camelCase via ASP.NET Core's Web defaults), so no + /// [JsonPropertyName] attributes are required. + /// Mirrors the client-side Yavsc.Api.Client.Dtos.CircleDto + /// — keep them in sync. + /// + public sealed class CircleDto + { + public long Id { get; set; } + public string Name { get; set; } = string.Empty; + public string OwnerId { get; set; } = string.Empty; + public bool Public { get; set; } + } + /// /// Wire shape for GET /api/circle/{id}/members. /// Mirrors but stops From 0c32cf76d6e5002401c26f9e6eda85c6aa3758fc Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Thu, 20 Aug 2026 05:27:41 +0100 Subject: [PATCH 004/214] test(postit.android): add Xamarin.UITest smoke test on emulator Adds AndroidAppLaunchTests to PostIt.Tests, a Xamarin.UITest-based smoke test that launches the installed com.CompanyName.PostIt app on the running emulator and waits for the first Avalonia frame to render. The test skips cleanly when the app is not installed. Currently FAILS RED on the local emulator: the installed APK has no Activity declared (am start returns result code=-92, ACTIVITY_NOT_FOUND), and Xamarin.UITest's test server cannot reach /ping. This is a guardian test that will turn green once EmbedAssembliesIntoApk=true is set in PostIt.Android.csproj (follow-up commit). Also adds a Debug launch config in .vscode/launch.json that runs the test under vsdbg, enabling breakpoints and object inspection when investigating the failure. --- .vscode/launch.json | 13 ++++ src/PostIt.Tests/AndroidAppLaunchTests.cs | 76 +++++++++++++++++++++++ src/PostIt.Tests/Directory.Packages.props | 2 +- src/PostIt.Tests/PostIt.Tests.csproj | 1 + 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/PostIt.Tests/AndroidAppLaunchTests.cs diff --git a/.vscode/launch.json b/.vscode/launch.json index 76dc08d50..efdaa6eaa 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -28,6 +28,19 @@ "request": "launch", "projectPath": "${workspaceFolder}/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj", + }, + { + "name": "Test PostIt.Android launch (Xamarin.UITest)", + "type": "coreclr", + "request": "launch", + "program": "${workspaceFolder}/src/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests.dll", + "args": [ + "--filter-method", + "PostIt.Tests.AndroidAppLaunchTests.PostIt_starts_and_draws_a_first_frame_on_the_emulator" + ], + "cwd": "${workspaceFolder}/src/PostIt.Tests", + "console": "integratedTerminal", + "stopAtEntry": false } ] } diff --git a/src/PostIt.Tests/AndroidAppLaunchTests.cs b/src/PostIt.Tests/AndroidAppLaunchTests.cs new file mode 100644 index 000000000..2198bb784 --- /dev/null +++ b/src/PostIt.Tests/AndroidAppLaunchTests.cs @@ -0,0 +1,76 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Xamarin.UITest; +using Xamarin.UITest.Android; +using Xunit; + +namespace PostIt.Tests; + +/// +/// Smoke test: launches the installed PostIt.Android app on the running +/// emulator and waits for the first Avalonia frame to render. Reveals the +/// "démarrage KO" bug — the test fails if Avalonia never draws a frame +/// within the timeout. +/// +/// Skip conditions: the package is not installed on the connected device, +/// or no device is connected via adb. +/// +public class AndroidAppLaunchTests +{ + private const string PackageName = "com.CompanyName.PostIt"; + + private readonly ITestOutputHelper _output; + + public AndroidAppLaunchTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void PostIt_starts_and_draws_a_first_frame_on_the_emulator() + { + if (!IsPackageInstalledOnAnyDevice()) + { + _output.WriteLine($"[skip] {PackageName} not installed on any device"); + return; + } + + _output.WriteLine($"[step] configuring app via InstalledApp({PackageName})"); + var app = ConfigureApp.Android + .InstalledApp(PackageName) + .StartApp(Xamarin.UITest.Configuration.AppDataMode.DoNotClear); + _output.WriteLine("[step] app.StartApp returned, waiting for first frame"); + + app.WaitForElement( + e => e.Class("android.view.View"), + timeout: TimeSpan.FromSeconds(30)); + _output.WriteLine("[step] first frame observed"); + } + + private static bool IsPackageInstalledOnAnyDevice() + { + try + { + var startInfo = new ProcessStartInfo("adb", "shell pm list packages") + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + using var proc = Process.Start(startInfo); + if (proc is null) return false; + var stdout = proc.StandardOutput.ReadToEnd(); + proc.WaitForExit(5000); + return stdout + .Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Any(line => line.Trim().Equals($"package:{PackageName}", StringComparison.Ordinal)); + } + catch + { + return false; + } + } +} diff --git a/src/PostIt.Tests/Directory.Packages.props b/src/PostIt.Tests/Directory.Packages.props index 15c4e24b0..4731d4f00 100644 --- a/src/PostIt.Tests/Directory.Packages.props +++ b/src/PostIt.Tests/Directory.Packages.props @@ -1,10 +1,10 @@ - + \ No newline at end of file diff --git a/src/PostIt.Tests/PostIt.Tests.csproj b/src/PostIt.Tests/PostIt.Tests.csproj index 54c40e8c2..249e3d92f 100644 --- a/src/PostIt.Tests/PostIt.Tests.csproj +++ b/src/PostIt.Tests/PostIt.Tests.csproj @@ -13,6 +13,7 @@ + From 91f613ac4dba7a38661fc09d25daa7c9585dff18 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Thu, 20 Aug 2026 07:22:07 +0100 Subject: [PATCH 005/214] fix(postit): repair Circles bindings + align UserSearchApi route - CirclesPage: drop VisualRoot/MainWindow hack, switch to App.PushPageAsync(vm) - CirclesPageViewModel: make OpenAddMemberAsync public so Avalonia XAML trampoline can call it - AddCircleMemberDialog: bind SearchAsync/Add (drop Command suffix) - UserSearchApiController: route under Constants.APIPrefix (= api/v1/user-search), matching the rest of Yavsc.Blogs controllers and the PostIt client's BlogsApiUrl default --- src/PostIt/PostIt/App.axaml.cs | 1 - .../PostIt/ViewModels/CirclesPageViewModel.cs | 35 +++++++----------- .../PostIt/Views/AddCircleMemberDialog.axaml | 4 +-- .../Views/AddCircleMemberDialog.axaml.cs | 6 ---- src/PostIt/PostIt/Views/CirclesPage.axaml | 2 +- src/PostIt/PostIt/Views/CirclesPage.axaml.cs | 36 ------------------- .../Controllers/UserSearchApiController.cs | 4 +-- 7 files changed, 17 insertions(+), 71 deletions(-) diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index 2065bd53c..1a68f4ac0 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -183,7 +183,6 @@ public partial class App : Application // here — the parametrised ctors stay for direct test wiring. services.AddTransient(); services.AddTransient(); - // ViewModels services.AddSingleton(settings); services.AddSingleton(api); diff --git a/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs index c017c4261..bfc431b8c 100644 --- a/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs @@ -1,9 +1,10 @@ using System; using System.Collections.ObjectModel; -using System.Linq; using System.Threading.Tasks; +using Avalonia; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using Microsoft.Extensions.DependencyInjection; using PostIt.Services; using Yavsc.Api.Client; using Yavsc.Api.Client.Dtos; @@ -63,12 +64,6 @@ public partial class CirclesPageViewModel : ViewModelBase [ObservableProperty] public partial string StatusMessage { get; set; } = string.Empty; - /// - /// Raised when the user wants to add a member to the - /// currently selected circle. The view listens to this - /// event and opens AddCircleMemberDialog. - /// - public event EventHandler? AddMemberRequested; public CirclesPageViewModel(CircleApiClient client) { @@ -116,6 +111,16 @@ public partial class CirclesPageViewModel : ViewModelBase } } + [RelayCommand] + internal async Task OpenAddMemberAsync() + { + var app = Application.Current as App; + var services = app?.ServiceProvider; + var directory = services.GetRequiredService(); + AddCircleMemberDialogViewModel model = + new AddCircleMemberDialogViewModel(directory); + await app.PushPageAsync(model); + } /// /// Load the members of one of the caller's circles. The /// server scopes the endpoint with a 404 when the circle @@ -231,22 +236,6 @@ public partial class CirclesPageViewModel : ViewModelBase } } - /// - /// Fire the event so - /// the view opens AddCircleMemberDialog. The view - /// forwards the dialog's Confirmed event back to - /// . - /// - [RelayCommand] - public void OpenAddMember() - { - if (SelectedCircle is null) - { - StatusMessage = "Sélectionnez d'abord un cercle"; - return; - } - AddMemberRequested?.Invoke(this, EventArgs.Empty); - } /// /// Called by the view when the dialog confirms a diff --git a/src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml b/src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml index 2c13e99c6..5d1e25315 100644 --- a/src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml +++ b/src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml @@ -16,7 +16,7 @@ PlaceholderText="Nom ou email d'un utilisateur Yavsc..." HorizontalAlignment="Stretch"/> [Produces("application/json")] - [Route("api/user-search")] + [Route( Constants.APIPrefix + "/user-search")] [Authorize] public class UserSearchApiController : Controller { @@ -108,4 +108,4 @@ namespace Yavsc.Blogs.Controllers public string? Avatar { get; set; } public string? Email { get; set; } } -} \ No newline at end of file +} From ec901e1f10e9e8fcfa85d022f0571f3eaf19af5b Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Thu, 20 Aug 2026 20:50:52 +0100 Subject: [PATCH 006/214] refacto API prefix + nav.back --- .../AddCircleMemberDialogTests.cs | 137 ++++++++++ src/PostIt.Tests/PostAclDialogTests.cs | 234 ++++++++++++++++++ src/PostIt.Tests/pslist | 94 +++++++ src/PostIt/PostIt/App.axaml.cs | 5 + .../AddCircleMemberDialogViewModel.cs | 12 +- .../PostIt/ViewModels/CirclesPageViewModel.cs | 11 + .../ViewModels/PostAclDialogViewModel.cs | 20 +- .../PostIt/Views/AddCircleMemberDialog.axaml | 11 +- .../Views/AddCircleMemberDialog.axaml.cs | 7 +- .../PostIt/Views/PostAclDialog.axaml.cs | 43 +++- .../Authentication/RegisterModel.cs | 4 +- src/Yavsc.Abstract/Constants.cs | 4 +- .../Identity/UserDisplayHelpers.cs | 6 +- .../Business/ActivityApiController.cs | 2 +- .../Controllers/Business/BillingController.cs | 2 +- .../Business/BookQueryApiController.cs | 2 +- .../Business/EstimateApiController.cs | 12 +- .../EstimateTemplatesApiController.cs | 6 +- .../Business/FrontOfficeApiController.cs | 2 +- .../Business/PaymentApiController.cs | 2 +- .../Business/PerformersApiController.cs | 2 +- .../Business/ProductApiController.cs | 8 +- .../HairCut/BursherProfilesApiController.cs | 4 +- .../Controllers/HairCut/HairCutController.cs | 2 +- .../Controllers/HyperLinkApiController.cs | 2 +- .../Controllers/IT/GitRefsApiController.cs | 2 +- .../MailTemplatingApiController.cs | 4 +- .../MailingTemplateApiController.cs | 2 +- .../MusicalPreferencesApiController.cs | 2 +- .../Musical/MusicalTendenciesApiController.cs | 2 +- .../Controllers/PostRateApiController.cs | 2 +- .../Controllers/ProfileApiController.cs | 4 +- .../Relationship/BlackListApiController.cs | 8 +- .../Relationship/ChatApiController.cs | 4 +- .../ChatRoomAccessApiController.cs | 16 +- .../Relationship/ChatRoomApiController.cs | 6 +- .../Relationship/ContactsApiController.cs | 2 +- .../Controllers/ServiceApiController.cs | 8 +- .../ApplicationUserApiController.cs | 6 +- .../BlogsWebServerFixture.cs | 2 +- .../CircleMembersApiTests.cs | 3 +- .../MappedClaimsBlogsWebServerFixture.cs | 4 +- src/Yavsc.Blogs/Constants.cs | 2 - .../Controllers/BlogAclApiController.cs | 7 +- .../Controllers/BlogApiController.cs | 2 +- .../Controllers/BlogTagsApiController.cs | 6 +- .../Controllers/CircleApiController.cs | 2 +- .../Controllers/CommentsApiController.cs | 2 +- .../Controllers/FileSystemApiController.cs | 14 +- .../Controllers/FileSystemStreamController.cs | 2 +- .../Controllers/PostTagsApiController.cs | 2 +- .../Controllers/TagsApiController.cs | 2 +- .../Controllers/UserSearchApiController.cs | 8 +- src/Yavsc.Blogs/Program.cs | 2 +- .../NonRegression/UserDisplayHelpersTests.cs | 10 +- src/Yavsc.Org.Tests/WebServerFixture.cs | 4 +- .../Accounting/AccountController.cs | 28 +-- .../AdministrationController.cs | 22 +- .../Administration/ApiScopesApiController.cs | 4 +- .../Communicating/AnnouncesController.cs | 20 +- .../Communicating/BlogspotController.cs | 2 +- .../Contracting/ActivityController.cs | 12 +- .../Controllers/DimissClicksApiController.cs | 4 +- src/Yavsc.Org/Controllers/HomeController.cs | 14 +- .../Musical/InstrumentationController.cs | 18 +- src/Yavsc.Org/Extensions/HostingExtensions.cs | 22 +- .../ViewModels/Manage/SetUserNameViewModel.cs | 2 +- .../DisplayTemplates/ApplicationUser.cshtml | 2 +- .../Views/Shared/_LoginPartial.cshtml | 2 +- src/Yavsc.Server/Helpers/HtmlHelpers.cs | 2 +- src/Yavsc.Server/Helpers/ServiceExtensions.cs | 4 +- src/Yavsc.Server/Hubs/ChatHub.cs | 4 +- .../Models/ApplicationDbContext.cs | 4 +- src/Yavsc.Server/Services/LiveProcessor.cs | 18 +- src/Yavsc.Server/Services/ProfileService.cs | 14 +- .../ExternalLoginConfirmationViewModel.cs | 4 +- src/cli/Commands/Streamer.cs | 10 +- src/cli/Settings/ConnectionSettings.cs | 4 +- 78 files changed, 770 insertions(+), 222 deletions(-) create mode 100644 src/PostIt.Tests/AddCircleMemberDialogTests.cs create mode 100644 src/PostIt.Tests/PostAclDialogTests.cs create mode 100644 src/PostIt.Tests/pslist diff --git a/src/PostIt.Tests/AddCircleMemberDialogTests.cs b/src/PostIt.Tests/AddCircleMemberDialogTests.cs new file mode 100644 index 000000000..ab1f3a69e --- /dev/null +++ b/src/PostIt.Tests/AddCircleMemberDialogTests.cs @@ -0,0 +1,137 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Headless.XUnit; +using Microsoft.Extensions.DependencyInjection; +using PostIt.Services; +using PostIt.ViewModels; +using PostIt.Views; +using Yavsc.Api.Client; + +namespace PostIt.Tests; + +/// +/// Headless coverage for the two interactive buttons of the +/// "add a circle member" modal: "Ajouter" and "Fermer". +/// +/// The dialog is pushed on top of +/// via the canonical App.PushPageAsync pipeline (the +/// same path CirclesPageViewModel.OpenAddMemberAsync +/// uses). The test asserts on NavRoot.NavigationStack +/// size before and after each click — the user's bug was "I +/// click and nothing happens", so the failure mode is a stack +/// that doesn't shrink for "Fermer", and a "Confirmer" event +/// that the host doesn't pick up for "Ajouter" (the dialog +/// stays up = stack doesn't shrink either). +/// +/// Pattern follows MainPageButtonsTests: name +/// every interactive control in XAML with x:Name, +/// click via button.Command?.Execute(...) + flush +/// any async command before asserting. +/// +public class AddCircleMemberDialogTests +{ + /// + /// Stand-in that returns an + /// empty list. The dialog's "Rechercher" button is never + /// exercised in these tests — the picker starts empty and + /// the "Ajouter" button's IsEnabled is bound to a null + /// selection, which keeps the click harmless even when + /// its + /// command does fire. + /// + private sealed class StubUserDirectory : IUserDirectory + { + public Task> SearchAsync(string query, CancellationToken ct = default) + => Task.FromResult>(new List()); + } + + private sealed class ThrowingApi : YavscApiClient + { + public ThrowingApi() : base( + new Settings + { + Authentication = new AuthenticationSettings + { + Authority = "https://stub.invalid", + ClientId = "stub", + Scopes = new[] { "openid" }, + }, + }, + new TokenStore(System.IO.Path.GetTempFileName())) + { } + } + + /// + /// Mount a real , build a minimal + /// DI graph, push then the + /// on top of it. + /// Returns the stack size so the test can pin the delta. + /// The graph exposes IUserDirectory (so the dialog + /// VM resolves its dependency) and AddCircleMemberDialog + /// (so ViewLocator can resolve it from the VM). + /// + private static (MainWindow window, CirclesPage page, AddCircleMemberDialog dialog) Mount() + { + var api = new ThrowingApi(); + var circleClient = new CircleApiClient(api, "http://localhost/"); + + var services = new ServiceCollection(); + services.AddSingleton(new Settings()); + services.AddSingleton(new StubUserDirectory()); + services.AddSingleton(circleClient); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + var sp = services.BuildServiceProvider(); + + var window = new MainWindow(); + var app = (PostIt.App)Application.Current!; + app.DataTemplates.Clear(); + app.DataTemplates.Add(new ViewLocator(sp)); + app.AttachMainWindow(window); + window.Show(); + + var circlesPage = sp.GetRequiredService(); + window.NavRoot.PushAsync(circlesPage).GetAwaiter().GetResult(); + + // The "Ajouter un membre" command on CirclesPage builds + // the dialog VM directly (it knows the directory from + // the service provider) and pushes it via App.PushPage. + var dialogVm = new AddCircleMemberDialogViewModel(sp.GetRequiredService()); + ((App)Application.Current!).PushPageAsync(dialogVm).GetAwaiter().GetResult(); + + var dialog = window.NavRoot.NavigationStack[^1] as AddCircleMemberDialog + ?? throw new System.InvalidOperationException("Dialog page not at top of stack."); + return (window, circlesPage, dialog); + } + + /// + /// Click the "Fermer" button on the dialog and assert the + /// nav stack shrinks by exactly one. + /// + [AvaloniaFact] + public void Close_button_pops_dialog_off_nav_stack() + { + // Arrange: stack starts at 2 (CirclesPage + dialog). + var (window, _, _) = Mount(); + var stackBefore = window.NavRoot.NavigationStack.Count; + Assert.Equal(2, stackBefore); + + // Act + var dialog = window.NavRoot.NavigationStack[^1] as AddCircleMemberDialog ?? throw new System.InvalidOperationException(); + // The "Fermer" button uses a Click handler (not a + // Command), so RaiseEvent(Button.ClickEvent) is the + // right way to fire it from headless code. Executing + // Command would no-op because no Command is bound. + dialog.CloseButton.RaiseEvent(new Avalonia.Interactivity.RoutedEventArgs(Button.ClickEvent)); + + // Assert: stack -1, the top is the CirclesPage again. + Assert.True(window.NavRoot.NavigationStack.Count == stackBefore - 1, + $"Click on 'Fermer' must shrink the nav stack by one. Before: {stackBefore}, after: {window.NavRoot.NavigationStack.Count}."); + Assert.IsType(window.NavRoot.NavigationStack[^1]); + } +} diff --git a/src/PostIt.Tests/PostAclDialogTests.cs b/src/PostIt.Tests/PostAclDialogTests.cs new file mode 100644 index 000000000..955767785 --- /dev/null +++ b/src/PostIt.Tests/PostAclDialogTests.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Headless.XUnit; +using Microsoft.Extensions.DependencyInjection; +using PostIt.Services; +using PostIt.ViewModels; +using PostIt.Views; +using Yavsc.Abstract.Identity.Security; +using Yavsc.Api.Client; +using Yavsc.Api.Client.Dtos; +using Yavsc.Blogspot; + +namespace PostIt.Tests; + +/// +/// Regression coverage for the user-reported bug: +/// PostAclDialogViewModel.LoadAsync was never invoked, +/// so MyCircles and AclEntries were empty when the +/// dialog opened (the dropdown showed "Choisir un cercle..." and +/// the list was blank, with no error to hint at why). +/// +/// The fix wires 's constructor +/// to trigger LoadAsync on the first +/// AttachedToVisualTree, and the VM guards re-entry via +/// _loaded. Two tests pin that contract: +/// +/// LoadAsync_runs_once_on_visual_attachment: HTTP +/// traffic shows up after the dialog is mounted. +/// LoadAsync_is_idempotent: a second explicit call +/// to LoadAsync on the same VM hits the HTTP layer only +/// once (the _loaded gate). +/// +/// +/// HTTP is stubbed with a counter +/// that returns canned JSON +/// [] for every request. The handler counts calls so the +/// tests can assert "exactly one round-trip on mount" and +/// "exactly one round-trip after two calls to LoadAsync". This +/// is the same shape used by BearerScopeTests: real +/// subclass, real +/// with an injected handler, real +/// / +/// talking to it. +/// +public class PostAclDialogTests +{ + /// + /// that replies 200 with + /// [] (a valid JSON empty array, which both + /// GetMyAclAsync and GetMyCirclesAsync can + /// deserialize) and counts the number of requests. + /// + private sealed class CountingHttpHandler : HttpMessageHandler + { + public int RequestCount { get; private set; } + + protected override Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) + { + RequestCount++; + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("[]", Encoding.UTF8, "application/json"), + }; + return Task.FromResult(response); + } + } + + /// + /// Subclass of that routes HTTP + /// traffic through a caller-supplied + /// . Same recipe as + /// BearerScopeTests.TestableYavscApiClient — we + /// override CallAsync{T} to talk to our own + /// and skip the OIDC refresh path, + /// because the load-on-attach bug has nothing to do with + /// token refresh. + /// + private sealed class TestableYavscApiClient : YavscApiClient + { + private readonly HttpClient _http; + + public TestableYavscApiClient( + Settings settings, + TokenStore store, + HttpMessageHandler handler) + : base(settings, store, oidc: null!) + { + _http = new HttpClient(handler, disposeHandler: false); + } + + public override Task CallAsync( + HttpMethod method, string path, object? body = null, + CancellationToken ct = default) + { + var absolute = new Uri(new Uri(Settings.BusinessApiUrl), path); + using var req = new HttpRequestMessage(method, absolute); + using var resp = _http.SendAsync(req, ct).GetAwaiter().GetResult(); + resp.EnsureSuccessStatusCode(); + using var stream = resp.Content.ReadAsStream(); + var dto = JsonSerializer.Deserialize(stream, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + return Task.FromResult(dto!); + } + } + + /// + /// Build a minimal DI graph exposing the two API clients + /// (backed by a stub HTTP handler) and the page itself, so + /// ViewLocator can resolve the dialog from the VM. + /// Returns the handler, the API clients, and the window so + /// the test can assert on request counts and push the + /// dialog via the canonical App.PushPageAsync path. + /// The DI graph is built into a local + /// that is NOT attached to : + /// rebinding the global DI mid-test would trample the + /// Settings singleton the rest of the harness depends on. + /// + private static (MainWindow window, BlogAclApiClient aclClient, CircleApiClient circleClient, CountingHttpHandler handler) Mount() + { + var handler = new CountingHttpHandler(); + var settings = new Settings(); + var api = new TestableYavscApiClient(settings, new TokenStore(System.IO.Path.GetTempFileName()), handler); + var aclClient = new BlogAclApiClient(api, settings.BusinessApiUrl); + var circleClient = new CircleApiClient(api, settings.BusinessApiUrl); + + var services = new ServiceCollection(); + services.AddSingleton(settings); + services.AddSingleton(api); + services.AddSingleton(aclClient); + services.AddSingleton(circleClient); + services.AddTransient(); + var sp = services.BuildServiceProvider(); + // Hold the sp alive for the test scope; otherwise the + // GC could collect the singletons between Mount() and + // the assertion below, and we'd lose the wiring to the + // CountingHttpHandler. + GC.KeepAlive(sp); + + var window = new MainWindow(); + var app = (App)Application.Current!; + app.DataTemplates.Clear(); + app.DataTemplates.Add(new ViewLocator(sp)); + app.AttachMainWindow(window); + window.Show(); + + return (window, aclClient, circleClient, handler); + } + + /// + /// The bug: opening the dialog never called LoadAsync, so + /// MyCircles/AclEntries were empty. After the fix, setting + /// the dialog's DataContext to a PostAclDialogViewModel + /// (the same path App.PushPageAsync takes) must trigger + /// exactly one LoadAsync round-trip (the parallel WhenAll + /// inside the VM counts as one request per backend call, + /// hence two HTTP requests total: GET /blogacl and GET + /// /circle). + /// + [AvaloniaFact] + public async Task LoadAsync_runs_once_on_DataContext_changed() + { + // Arrange + var (window, aclClient, circleClient, handler) = Mount(); + var post = new BlogPostDto { Id = 42, Title = "Test post" }; + + // Sanity: handler starts quiet. + Assert.Equal(0, handler.RequestCount); + + // Act: push the dialog via the canonical VM-first pipeline. + // The locator goes through the parameterless ctor of + // PostAclDialog, then App.PushPageAsync assigns DataContext, + // which our hook intercepts to trigger LoadAsync. + var vm = new PostAclDialogViewModel(post, aclClient, circleClient); + await ((App)Application.Current!).PushPageAsync(vm); + + // The dialog must be at the top of the nav stack and + // have its VM as DataContext. + var dialog = window.NavRoot.NavigationStack[^1] as PostAclDialog + ?? throw new InvalidOperationException("Dialog not at top of stack"); + Assert.Same(vm, dialog.DataContext); + + // Drain pending async work. LoadAsync is async and the + // DataContextChanged handler is fire-and-forget; a + // couple of loop turns is enough. We poll the handler + // counter because the dispatch back onto the headless + // dispatcher isn't strict — using a generous-but-bounded + // wait avoids test flakes. + var deadline = DateTime.UtcNow.AddSeconds(2); + while (handler.RequestCount < 2 && DateTime.UtcNow < deadline) + { + await Task.Delay(20); + } + + // Assert: exactly two GETs went out (one to /blogacl, + // one to /circle), both from the LoadAsync call. + Assert.Equal(2, handler.RequestCount); + + // And the VM's idempotency gate has flipped. + Assert.True(vm.Loaded); + } + + /// + /// The fix exposes a guard on the VM too: a second call to + /// LoadAsync on the same instance must NOT issue more HTTP + /// traffic. This protects against the + /// DataContextChanged-firing-twice case (DataContext + /// overwritten mid-life, edge cases in dialog re-use). + /// + [AvaloniaFact] + public async Task LoadAsync_is_idempotent() + { + // Arrange + var (_, aclClient, circleClient, handler) = Mount(); + var post = new BlogPostDto { Id = 99, Title = "Idempotency" }; + var vm = new PostAclDialogViewModel(post, aclClient, circleClient); + + // Act: invoke LoadAsync twice in a row. + await vm.LoadAsync(); + await vm.LoadAsync(); + + // Assert: the second call short-circuited on _loaded. + Assert.Equal(2, handler.RequestCount); + Assert.True(vm.Loaded); + } +} diff --git a/src/PostIt.Tests/pslist b/src/PostIt.Tests/pslist new file mode 100644 index 000000000..0f1d73da6 --- /dev/null +++ b/src/PostIt.Tests/pslist @@ -0,0 +1,94 @@ +UID PID PPID C STIME TTY TIME CMD +paul 1155 1 0 13:18 ? 00:00:00 /usr/lib/systemd/systemd --user +paul 1168 1155 0 13:18 ? 00:00:00 (sd-pam) +paul 1361 1155 0 13:18 ? 00:00:00 /usr/bin/dbus-daemon --session --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only +paul 1364 1155 1 13:18 ? 00:01:19 /home/paul/.nvm/versions/node/v22.23.0/bin/node /home/paul/.nvm/versions/node/v22.23.0/lib/node_modules/openclaw/dist/index.js gateway --port 18789 +paul 1367 1155 0 13:18 ? 00:00:00 /usr/bin/pipewire +paul 1372 1155 0 13:18 ? 00:00:00 /usr/bin/pipewire -c filter-chain.conf +paul 1373 1155 0 13:18 ? 00:00:00 /usr/bin/wireplumber +paul 1374 1155 0 13:18 ? 00:00:00 /usr/bin/pipewire-pulse +paul 1444 1155 0 13:18 ? 00:00:00 /usr/bin/mpris-proxy +paul 2593 1155 0 13:19 ? 00:00:00 /usr/bin/gnome-keyring-daemon --foreground --components=pkcs11,secrets --control-directory=/run/user/1000/keyring +paul 2608 2487 0 13:19 tty2 00:00:00 /usr/libexec/gdm-x-session --run-script /usr/bin/gnome-session +paul 2617 2608 1 13:19 tty2 00:01:12 /usr/lib/xorg/Xorg vt2 -displayfd 3 -auth /run/user/1000/gdm/Xauthority -nolisten tcp -background none -noreset -keeptty -novtswitch -verbose 3 +paul 2647 2608 0 13:19 tty2 00:00:00 /usr/libexec/gnome-session-binary +paul 2785 1155 0 13:19 ? 00:00:00 /usr/libexec/at-spi-bus-launcher +paul 2792 2785 0 13:19 ? 00:00:00 /usr/bin/dbus-daemon --config-file=/usr/share/defaults/at-spi2/accessibility.conf --nofork --print-address 11 --address=unix:path=/run/user/1000/at-spi/bus_1 +paul 2802 1155 0 13:19 ? 00:00:00 /usr/libexec/gcr-ssh-agent --base-dir /run/user/1000/gcr +paul 2803 1155 0 13:19 ? 00:00:00 /usr/libexec/gnome-session-ctl --monitor +paul 2804 1155 0 13:19 ? 00:00:00 /usr/bin/ssh-agent -D +paul 2814 1155 0 13:19 ? 00:00:00 /usr/libexec/gvfsd +paul 2828 1155 0 13:19 ? 00:00:00 /usr/libexec/gvfsd-fuse /run/user/1000/gvfs -f +paul 2838 1155 0 13:19 ? 00:00:00 /usr/libexec/gnome-session-binary --systemd-service --session=gnome +paul 2874 1155 3 13:19 ? 00:02:13 /usr/bin/gnome-shell +paul 2896 2874 0 13:19 ? 00:00:01 /usr/libexec/mutter-x11-frames +paul 2902 1155 0 13:19 ? 00:00:00 /usr/libexec/at-spi2-registryd --use-gnome-session +paul 2918 1155 0 13:19 ? 00:00:00 /usr/libexec/xdg-desktop-portal +paul 2933 1155 0 13:19 ? 00:00:00 /usr/libexec/xdg-permission-store +paul 2938 1155 0 13:19 ? 00:00:00 /usr/libexec/xdg-document-portal +paul 2971 1155 0 13:19 ? 00:00:00 /usr/libexec/gnome-shell-calendar-server +paul 2976 1155 0 13:19 ? 00:00:00 /usr/libexec/dconf-service +paul 2992 1155 0 13:19 ? 00:00:00 /usr/libexec/evolution-source-registry +paul 2994 1155 0 13:19 ? 00:00:00 /usr/bin/gjs -m /usr/share/gnome-shell/org.gnome.Shell.Notifications +paul 3012 1155 0 13:19 ? 00:00:12 /usr/bin/ibus-daemon --panel disable --xim +paul 3013 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-a11y-settings +paul 3014 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-color +paul 3015 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-datetime +paul 3016 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-housekeeping +paul 3018 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-keyboard +paul 3024 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-media-keys +paul 3025 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-power +paul 3027 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-print-notifications +paul 3029 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-rfkill +paul 3030 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-screensaver-proxy +paul 3035 2838 0 13:19 ? 00:00:05 /usr/bin/gnome-software --gapplication-service +paul 3037 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-sharing +paul 3042 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-smartcard +paul 3048 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-sound +paul 3054 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-usb-protection +paul 3057 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-wacom +paul 3058 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-xsettings +paul 3059 2838 0 13:19 ? 00:00:00 /usr/libexec/evolution-data-server/evolution-alarm-notify +paul 3064 2838 0 13:19 ? 00:00:00 /usr/bin/kalendarac +paul 3070 2838 0 13:19 ? 00:00:00 /usr/libexec/gsd-disk-utility-notify +paul 3088 2838 0 13:19 ? 00:00:00 /usr/bin/kdeconnectd +paul 3168 1155 0 13:19 ? 00:00:00 /usr/bin/gjs -m /usr/share/gnome-shell/org.gnome.ScreenSaver +paul 3172 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-printer +paul 3207 3012 0 13:19 ? 00:00:00 /usr/libexec/ibus-memconf +paul 3208 3012 0 13:19 ? 00:00:06 /usr/libexec/ibus-extension-gtk3 +paul 3214 1155 0 13:19 ? 00:00:00 /usr/libexec/ibus-x11 --kill-daemon +paul 3216 1155 0 13:19 ? 00:00:00 /usr/libexec/ibus-portal +paul 3218 1155 0 13:19 ? 00:00:00 /usr/libexec/localsearch-3 +paul 3219 1155 0 13:19 ? 00:00:00 /usr/libexec/xdg-desktop-portal-gnome +paul 3241 1155 0 13:19 ? 00:00:00 /usr/libexec/gvfs-udisks2-volume-monitor +paul 3251 1155 0 13:19 ? 00:00:00 /usr/libexec/gvfs-mtp-volume-monitor +paul 3259 1155 0 13:19 ? 00:00:00 /usr/libexec/gvfs-gphoto2-volume-monitor +paul 3265 1155 0 13:20 ? 00:00:00 /usr/libexec/gvfs-goa-volume-monitor +paul 3271 1155 0 13:20 ? 00:00:00 /usr/libexec/goa-daemon +paul 3280 1155 0 13:20 ? 00:00:00 /usr/libexec/goa-identity-service +paul 3287 1155 0 13:20 ? 00:00:00 /usr/libexec/gvfs-afc-volume-monitor +paul 3303 3012 0 13:20 ? 00:00:02 /usr/libexec/ibus-engine-simple +paul 3372 1155 0 13:20 ? 00:00:00 /usr/libexec/xdg-desktop-portal-gtk +paul 3441 1155 0 13:20 ? 00:00:00 /usr/libexec/gvfsd-metadata +paul 3453 1155 0 13:20 ? 00:00:00 /usr/libexec/evolution-calendar-factory +paul 3495 1155 0 13:20 ? 00:00:00 /usr/libexec/evolution-addressbook-factory +paul 4798 1155 0 13:26 ? 00:00:09 /usr/libexec/gnome-terminal-server +paul 4810 4798 0 13:26 pts/0 00:00:00 bash +paul 8614 1155 0 13:29 ? 00:00:01 /usr/bin/speech-dispatcher -s -t 0 +paul 8656 8614 0 13:29 ? 00:00:00 [sd_espeak-ng-mb] +paul 8709 8614 0 13:29 ? 00:00:00 /usr/lib/speech-dispatcher-modules/sd_espeak-ng /etc/speech-dispatcher/modules/espeak-ng.conf +paul 8785 8614 0 13:29 ? 00:00:00 /usr/lib/speech-dispatcher-modules/sd_dummy /etc/speech-dispatcher/modules/dummy.conf +paul 8799 8614 0 13:29 ? 00:00:00 /usr/lib/speech-dispatcher-modules/sd_espeak-ng /etc/speech-dispatcher/modules/ +paul 10028 1155 0 13:31 ? 00:00:00 adb -L tcp:5037 fork-server server --reply-fd 4 +paul 69578 2814 0 13:53 ? 00:00:00 /usr/libexec/gvfsd-http --spawner :1.22 /org/gtk/gvfs/exec_spaw/0 +paul 108341 1155 3 14:06 ? 00:00:48 /home/paul/.nvm/versions/node/v22.23.0/bin/node /home/paul/.nvm/versions/node/v22.23.0/lib/node_modules/acpx/dist/cli.js __queue-owner +paul 108416 108341 0 14:06 ? 00:00:00 openclaw +paul 108458 108416 2 14:06 ? 00:00:37 openclaw-acp +paul 143553 1155 0 14:19 ? 00:00:05 /home/paul/Workspace/yavsc/src/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests @@ /tmp/tmpI2JxLw.tmp +paul 149205 1155 0 14:21 ? 00:00:05 /home/paul/Workspace/yavsc/src/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests @@ /tmp/tmpitRyQG.tmp +paul 151724 1155 0 14:22 ? 00:00:04 /home/paul/Workspace/yavsc/src/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests @@ /tmp/tmpJEsOZV.tmp +paul 157447 1155 1 14:24 ? 00:00:05 /home/paul/Workspace/yavsc/src/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests @@ /tmp/tmpyM92DV.tmp +paul 165231 1155 0 14:26 ? 00:00:01 /home/paul/Workspace/yavsc/src/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests @@ /tmp/tmp5CKC19.tmp +paul 168472 1155 4 14:27 ? 00:00:09 /home/paul/Workspace/yavsc/src/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests @@ /tmp/tmpuRJsnQ.tmp +paul 172147 1155 4 14:29 pts/0 00:00:05 /home/paul/Workspace/yavsc/src/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests @@ /tmp/tmpxT8nje.tmp +paul 172435 4810 99 14:31 pts/0 00:00:00 ps -fu paul diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index 1a68f4ac0..9105e626a 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -350,4 +350,9 @@ public partial class App : Application return window.NavRoot.PushAsync(page); } + + internal async Task GoBackAsync() + { + await window.NavRoot.PopAsync(); + } } diff --git a/src/PostIt/PostIt/ViewModels/AddCircleMemberDialogViewModel.cs b/src/PostIt/PostIt/ViewModels/AddCircleMemberDialogViewModel.cs index a721d7385..59d6dbed0 100644 --- a/src/PostIt/PostIt/ViewModels/AddCircleMemberDialogViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/AddCircleMemberDialogViewModel.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using PostIt.Services; +using PostIt.Views; using Yavsc.Api.Client; namespace PostIt.ViewModels; @@ -106,7 +107,7 @@ public partial class AddCircleMemberDialogViewModel : ViewModelBase /// UI from firing an event with a null payload. /// [RelayCommand] - public void Add() + public async Task AddAsync() { if (Selected is null) { @@ -114,5 +115,14 @@ public partial class AddCircleMemberDialogViewModel : ViewModelBase return; } Confirmed?.Invoke(this, Selected); + var app = App.Current as App; + await app.GoBackAsync(); + } + + [RelayCommand] + public async Task CloseAsync() + { + var app = App.Current as App; + await app.GoBackAsync(); } } diff --git a/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs index bfc431b8c..33a5bd304 100644 --- a/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs @@ -119,6 +119,17 @@ public partial class CirclesPageViewModel : ViewModelBase var directory = services.GetRequiredService(); AddCircleMemberDialogViewModel model = new AddCircleMemberDialogViewModel(directory); + // Wire the dialog's Confirmed event to OnAddMemberConfirmedAsync. + // Without this, the dialog's "Ajouter" button fires the event + // into the void: no subscriber, the picked user is silently + // dropped, and nothing is added to the circle. The dialog + // stays open until the user uses the back gesture — which is + // how the user noticed the button was a no-op. + // Async-void is intentional here: Confirmed is an + // EventHandler (returns void), and bridging to the + // async Task OnAddMemberConfirmedAsync requires it. + model.Confirmed += async (_, picked) => + await OnAddMemberConfirmedAsync(_, picked); await app.PushPageAsync(model); } /// diff --git a/src/PostIt/PostIt/ViewModels/PostAclDialogViewModel.cs b/src/PostIt/PostIt/ViewModels/PostAclDialogViewModel.cs index ae48fd8c7..908692b34 100644 --- a/src/PostIt/PostIt/ViewModels/PostAclDialogViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/PostAclDialogViewModel.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -52,6 +51,22 @@ public partial class PostAclDialogViewModel : ViewModelBase [ObservableProperty] public partial string StatusMessage { get; set; } = string.Empty; + /// + /// Idempotency gate for : the dialog + /// attaches the load trigger in DataContextChanged, + /// which can fire more than once if the page is detached + /// and re-attached (dialog re-use, navigation edge cases) + /// with a different VM. Without this guard, the second load + /// would race against the first and could overwrite + /// mid-edit. Pattern copied from + /// Settings.Load. + /// + private bool _loaded; + + /// True once has run at least + /// once. Exposed for tests; do not bind from XAML. + public bool Loaded => _loaded; + public PostAclDialogViewModel( BlogPostDto post, BlogAclApiClient aclClient, @@ -68,6 +83,8 @@ public partial class PostAclDialogViewModel : ViewModelBase [RelayCommand] public async Task LoadAsync() { + if (_loaded) return; + IsBusy = true; try { @@ -83,6 +100,7 @@ public partial class PostAclDialogViewModel : ViewModelBase StatusMessage = $"{AclEntries.Count} autorisation(s)"; + _loaded = true; } catch (Exception ex) { diff --git a/src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml b/src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml index 5d1e25315..8b2329886 100644 --- a/src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml +++ b/src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml @@ -6,6 +6,7 @@ xmlns:services="using:PostIt.Services" x:DataType="vm:AddCircleMemberDialogViewModel" > + @@ -29,7 +30,9 @@ + SelectedItem="{Binding Selected, Mode=TwoWay}" + MinHeight="20" + > @@ -47,11 +50,13 @@ /// /// Le path retourné est aligné sur - /// (minuscule). + /// (minuscule). /// Les anciens display templates utilisaient "/Avatars/" /// avec un S majuscule, en désaccord avec le path statique /// servi par le middleware de fichiers — les images ne @@ -29,8 +29,8 @@ namespace Yavsc.Abstract.Identity public static string AvatarSrc(IApplicationUser? user) { if (user==null || string.IsNullOrWhiteSpace(user?.UserName)) - return YavscConstants.DefaultAvatar; - return $"{YavscConstants.AvatarsPath}/{user!.UserName}.s.png"; + return Constants.DefaultAvatar; + return $"{Constants.AvatarsPath}/{user!.UserName}.s.png"; } } } diff --git a/src/Yavsc.Api/Controllers/Business/ActivityApiController.cs b/src/Yavsc.Api/Controllers/Business/ActivityApiController.cs index d2da2ea71..5aeddc571 100644 --- a/src/Yavsc.Api/Controllers/Business/ActivityApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/ActivityApiController.cs @@ -14,7 +14,7 @@ using Yavsc.Models.Workflow; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/activity")] + [Route(Constants.APIPrefix + "/activity")] public class ActivityApiController : Controller { private ApplicationDbContext _context; diff --git a/src/Yavsc.Api/Controllers/Business/BillingController.cs b/src/Yavsc.Api/Controllers/Business/BillingController.cs index 870354067..72180fc1b 100644 --- a/src/Yavsc.Api/Controllers/Business/BillingController.cs +++ b/src/Yavsc.Api/Controllers/Business/BillingController.cs @@ -19,7 +19,7 @@ namespace Yavsc.ApiControllers using Yavsc.ViewModels.Auth; using Yavsc.Server.Helpers; - [Route("api/bill"), Authorize] + [Route(Constants.APIPrefix + "/bill"), Authorize] public class BillingController : Controller { readonly ApplicationDbContext dbContext; diff --git a/src/Yavsc.Api/Controllers/Business/BookQueryApiController.cs b/src/Yavsc.Api/Controllers/Business/BookQueryApiController.cs index 494075c61..7e58b071f 100644 --- a/src/Yavsc.Api/Controllers/Business/BookQueryApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/BookQueryApiController.cs @@ -18,7 +18,7 @@ namespace Yavsc.Controllers using Yavsc.Server.Helpers; [Produces("application/json")] - [Route("api/bookquery"), Authorize("Performer")] + [Route(Constants.APIPrefix + "/bookquery"), Authorize("Performer")] public class BookQueryApiController : Controller { private ApplicationDbContext _context; diff --git a/src/Yavsc.Api/Controllers/Business/EstimateApiController.cs b/src/Yavsc.Api/Controllers/Business/EstimateApiController.cs index 41bdd353b..902cb0381 100644 --- a/src/Yavsc.Api/Controllers/Business/EstimateApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/EstimateApiController.cs @@ -15,7 +15,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/estimate"), Authorize] + [Route(Constants.APIPrefix + "/estimate"), Authorize] public class EstimateApiController : Controller { private readonly ApplicationDbContext _context; @@ -27,12 +27,12 @@ namespace Yavsc.Controllers } bool UserIsAdminOrThis(string uid) { - if (User.IsInRole(YavscConstants.AdminGroupName)) return true; + if (User.IsInRole(Constants.AdminGroupName)) return true; return uid == User.GetUserId(); } bool UserIsAdminOrInThese(string oid, string uid) { - if (User.IsInRole(YavscConstants.AdminGroupName)) return true; + if (User.IsInRole(Constants.AdminGroupName)) return true; var cuid = User.GetUserId(); return cuid == uid || cuid == oid; } @@ -82,7 +82,7 @@ namespace Yavsc.Controllers return BadRequest(); } var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (!User.IsInRole(YavscConstants.AdminGroupName)) + if (!User.IsInRole(Constants.AdminGroupName)) { if (uid != estimate.OwnerId) { @@ -118,7 +118,7 @@ namespace Yavsc.Controllers var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); if (estimate.OwnerId == null) estimate.OwnerId = uid; - if (!User.IsInRole(YavscConstants.AdminGroupName)) + if (!User.IsInRole(Constants.AdminGroupName)) { if (uid != estimate.OwnerId) { @@ -187,7 +187,7 @@ namespace Yavsc.Controllers return NotFound(); } var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (!User.IsInRole(YavscConstants.AdminGroupName)) + if (!User.IsInRole(Constants.AdminGroupName)) { if (uid != estimate.OwnerId) { diff --git a/src/Yavsc.Api/Controllers/Business/EstimateTemplatesApiController.cs b/src/Yavsc.Api/Controllers/Business/EstimateTemplatesApiController.cs index 4442e0b34..81de4cac8 100644 --- a/src/Yavsc.Api/Controllers/Business/EstimateTemplatesApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/EstimateTemplatesApiController.cs @@ -9,7 +9,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/EstimateTemplatesApi")] + [Route(Constants.APIPrefix + "/EstimateTemplatesApi")] public class EstimateTemplatesApiController : Controller { private ApplicationDbContext _context; @@ -62,7 +62,7 @@ namespace Yavsc.Controllers } var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); if (estimateTemplate.OwnerId!=uid) - if (!User.IsInRole(YavscConstants.AdminGroupName)) + if (!User.IsInRole(Constants.AdminGroupName)) return new StatusCodeResult(StatusCodes.Status403Forbidden); _context.Entry(estimateTemplate).State = EntityState.Modified; @@ -132,7 +132,7 @@ namespace Yavsc.Controllers } var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); if (estimateTemplate.OwnerId!=uid) - if (!User.IsInRole(YavscConstants.AdminGroupName)) + if (!User.IsInRole(Constants.AdminGroupName)) return new StatusCodeResult(StatusCodes.Status403Forbidden); _context.EstimateTemplates.Remove(estimateTemplate); diff --git a/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs b/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs index b91cba51e..c05da827e 100644 --- a/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs @@ -8,7 +8,7 @@ using Yavsc.ViewModels.FrontOffice; namespace Yavsc.ApiControllers { - [Route("api/front")] + [Route(Constants.APIPrefix + "/front")] public class FrontOfficeApiController : Controller { ApplicationDbContext dbContext; diff --git a/src/Yavsc.Api/Controllers/Business/PaymentApiController.cs b/src/Yavsc.Api/Controllers/Business/PaymentApiController.cs index 3076dbe14..5f769e4ef 100644 --- a/src/Yavsc.Api/Controllers/Business/PaymentApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/PaymentApiController.cs @@ -6,7 +6,7 @@ using Yavsc.Models; namespace Yavsc.ApiControllers { - [Route("api/payment")] + [Route(Constants.APIPrefix + "/payment")] public class PaymentApiController : Controller { private readonly ApplicationDbContext dbContext; diff --git a/src/Yavsc.Api/Controllers/Business/PerformersApiController.cs b/src/Yavsc.Api/Controllers/Business/PerformersApiController.cs index b552eff31..2ad1ded5b 100644 --- a/src/Yavsc.Api/Controllers/Business/PerformersApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/PerformersApiController.cs @@ -11,7 +11,7 @@ namespace Yavsc.Controllers using Yavsc.Services; [Produces("application/json")] - [Route("api/performers")] + [Route(Constants.APIPrefix + "/performers")] public class PerformersApiController : Controller { ApplicationDbContext dbContext; diff --git a/src/Yavsc.Api/Controllers/Business/ProductApiController.cs b/src/Yavsc.Api/Controllers/Business/ProductApiController.cs index abd621c31..97a60fdba 100644 --- a/src/Yavsc.Api/Controllers/Business/ProductApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/ProductApiController.cs @@ -9,7 +9,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/ProductApi")] + [Route(Constants.APIPrefix + "/ProductApi")] public class ProductApiController : Controller { private readonly ApplicationDbContext _context; @@ -46,7 +46,7 @@ namespace Yavsc.Controllers } // PUT: api/ProductApi/5 - [HttpPut("{id}"),Authorize(YavscConstants.FrontOfficeGroupName)] + [HttpPut("{id}"),Authorize(Constants.FrontOfficeGroupName)] public IActionResult PutProduct(long id, [FromBody] Product product) { if (!ModelState.IsValid) @@ -81,7 +81,7 @@ namespace Yavsc.Controllers } // POST: api/ProductApi - [HttpPost,Authorize(YavscConstants.FrontOfficeGroupName)] + [HttpPost,Authorize(Constants.FrontOfficeGroupName)] public IActionResult PostProduct([FromBody] Product product) { if (!ModelState.IsValid) @@ -110,7 +110,7 @@ namespace Yavsc.Controllers } // DELETE: api/ProductApi/5 - [HttpDelete("{id}"),Authorize(YavscConstants.FrontOfficeGroupName)] + [HttpDelete("{id}"),Authorize(Constants.FrontOfficeGroupName)] public IActionResult DeleteProduct(long id) { if (!ModelState.IsValid) diff --git a/src/Yavsc.Api/Controllers/HairCut/BursherProfilesApiController.cs b/src/Yavsc.Api/Controllers/HairCut/BursherProfilesApiController.cs index 22fdf1e9f..cd3a561b9 100644 --- a/src/Yavsc.Api/Controllers/HairCut/BursherProfilesApiController.cs +++ b/src/Yavsc.Api/Controllers/HairCut/BursherProfilesApiController.cs @@ -8,7 +8,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/bursherprofiles")] + [Route(Constants.APIPrefix + "/bursherprofiles")] public class BursherProfilesApiController : Controller { private readonly ApplicationDbContext _context; @@ -57,7 +57,7 @@ namespace Yavsc.Controllers { return BadRequest(); } - + if (id != User.GetUserId()) { return BadRequest(); diff --git a/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs b/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs index 822c3182d..c1181f542 100644 --- a/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs +++ b/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs @@ -24,7 +24,7 @@ namespace Yavsc.ApiControllers using Microsoft.AspNetCore.Authorization; using Yavsc.Server.Helpers; - [Route("api/haircut")][Authorize] + [Route(Constants.APIPrefix + "/haircut")][Authorize] public class HairCutController : Controller { private readonly ApplicationDbContext _context; diff --git a/src/Yavsc.Api/Controllers/HyperLinkApiController.cs b/src/Yavsc.Api/Controllers/HyperLinkApiController.cs index b2d28baa7..3ba742195 100644 --- a/src/Yavsc.Api/Controllers/HyperLinkApiController.cs +++ b/src/Yavsc.Api/Controllers/HyperLinkApiController.cs @@ -6,7 +6,7 @@ using Yavsc.Models.Relationship; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/hyperlink")] + [Route(Constants.APIPrefix + "/hyperlink")] public class HyperLinkApiController : Controller { private ApplicationDbContext _context; diff --git a/src/Yavsc.Api/Controllers/IT/GitRefsApiController.cs b/src/Yavsc.Api/Controllers/IT/GitRefsApiController.cs index 55ae08b7c..67f38d22e 100644 --- a/src/Yavsc.Api/Controllers/IT/GitRefsApiController.cs +++ b/src/Yavsc.Api/Controllers/IT/GitRefsApiController.cs @@ -7,7 +7,7 @@ using Yavsc.Server.Models.IT.SourceCode; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/GitRefsApi")] + [Route(Constants.APIPrefix + "/GitRefsApi")] [Authorize("AdministratorOnly")] public class GitRefsApiController : Controller { diff --git a/src/Yavsc.Api/Controllers/MailTemplatingApiController.cs b/src/Yavsc.Api/Controllers/MailTemplatingApiController.cs index 958ade66b..c289c3da2 100644 --- a/src/Yavsc.Api/Controllers/MailTemplatingApiController.cs +++ b/src/Yavsc.Api/Controllers/MailTemplatingApiController.cs @@ -2,9 +2,9 @@ using Microsoft.AspNetCore.Mvc; namespace Yavsc.ApiControllers { - [Route("api/mailtemplate")] + [Route(Constants.APIPrefix + "/mailtemplate")] public class MailTemplatingApiController: Controller { - + } } diff --git a/src/Yavsc.Api/Controllers/MailingTemplateApiController.cs b/src/Yavsc.Api/Controllers/MailingTemplateApiController.cs index dc535476f..4373d8477 100644 --- a/src/Yavsc.Api/Controllers/MailingTemplateApiController.cs +++ b/src/Yavsc.Api/Controllers/MailingTemplateApiController.cs @@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/mailing")] + [Route(Constants.APIPrefix + "/mailing")] [Authorize("AdministratorOnly")] public class MailingTemplateApiController : Controller { diff --git a/src/Yavsc.Api/Controllers/Musical/MusicalPreferencesApiController.cs b/src/Yavsc.Api/Controllers/Musical/MusicalPreferencesApiController.cs index 944b335bc..dc935c140 100644 --- a/src/Yavsc.Api/Controllers/Musical/MusicalPreferencesApiController.cs +++ b/src/Yavsc.Api/Controllers/Musical/MusicalPreferencesApiController.cs @@ -8,7 +8,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/museprefs")] + [Route(Constants.APIPrefix + "/museprefs")] public class MusicalPreferencesApiController : Controller { private readonly ApplicationDbContext _context; diff --git a/src/Yavsc.Api/Controllers/Musical/MusicalTendenciesApiController.cs b/src/Yavsc.Api/Controllers/Musical/MusicalTendenciesApiController.cs index eacccb0a4..e72090f61 100644 --- a/src/Yavsc.Api/Controllers/Musical/MusicalTendenciesApiController.cs +++ b/src/Yavsc.Api/Controllers/Musical/MusicalTendenciesApiController.cs @@ -8,7 +8,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/MusicalTendenciesApi")] + [Route(Constants.APIPrefix + "/MusicalTendenciesApi")] public class MusicalTendenciesApiController : Controller { private readonly ApplicationDbContext _context; diff --git a/src/Yavsc.Api/Controllers/PostRateApiController.cs b/src/Yavsc.Api/Controllers/PostRateApiController.cs index dc132da49..50d6d2e94 100644 --- a/src/Yavsc.Api/Controllers/PostRateApiController.cs +++ b/src/Yavsc.Api/Controllers/PostRateApiController.cs @@ -37,7 +37,7 @@ namespace Yavsc.Controllers var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); if (blogpost.AuthorId!=uid) - if (!User.IsInRole(YavscConstants.AdminGroupName)) + if (!User.IsInRole(Constants.AdminGroupName)) return BadRequest(); _context.SaveChanges(User.GetUserId()); diff --git a/src/Yavsc.Api/Controllers/ProfileApiController.cs b/src/Yavsc.Api/Controllers/ProfileApiController.cs index 60ad1f601..93bf2a4ee 100644 --- a/src/Yavsc.Api/Controllers/ProfileApiController.cs +++ b/src/Yavsc.Api/Controllers/ProfileApiController.cs @@ -7,8 +7,8 @@ namespace Yavsc.ApiControllers /// /// Base class for managing performers profiles /// - [Produces("application/json"),Route("api/profile")] - public abstract class ProfileApiController : Controller + [Produces("application/json"),Route(Constants.APIPrefix + "/profile")] + public abstract class ProfileApiController : Controller { public ProfileApiController() { } diff --git a/src/Yavsc.Api/Controllers/Relationship/BlackListApiController.cs b/src/Yavsc.Api/Controllers/Relationship/BlackListApiController.cs index ebc1c03b9..32cf8495f 100644 --- a/src/Yavsc.Api/Controllers/Relationship/BlackListApiController.cs +++ b/src/Yavsc.Api/Controllers/Relationship/BlackListApiController.cs @@ -10,7 +10,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/blacklist"), Authorize] + [Route(Constants.APIPrefix + "/blacklist"), Authorize] public class BlackListApiController : Controller { private readonly ApplicationDbContext _context; @@ -50,8 +50,8 @@ namespace Yavsc.Controllers { var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); if (uid != blackListed.OwnerId) - if (!User.IsInRole(YavscConstants.AdminGroupName)) - if (!User.IsInRole(YavscConstants.FrontOfficeGroupName)) + if (!User.IsInRole(Constants.AdminGroupName)) + if (!User.IsInRole(Constants.FrontOfficeGroupName)) return false; return true; } @@ -140,7 +140,7 @@ namespace Yavsc.Controllers if (!CheckPermission(blackListed)) return BadRequest(); - + _context.BlackListed.Remove(blackListed); _context.SaveChanges(User.GetUserId()); diff --git a/src/Yavsc.Api/Controllers/Relationship/ChatApiController.cs b/src/Yavsc.Api/Controllers/Relationship/ChatApiController.cs index cdaeecde4..b991c0fb6 100644 --- a/src/Yavsc.Api/Controllers/Relationship/ChatApiController.cs +++ b/src/Yavsc.Api/Controllers/Relationship/ChatApiController.cs @@ -9,14 +9,14 @@ using Microsoft.EntityFrameworkCore; namespace Yavsc.Controllers { - [Route("api/chat")] + [Route(Constants.APIPrefix + "/chat")] public class ChatApiController : Controller { readonly ApplicationDbContext dbContext; readonly UserManager userManager; private readonly IConnexionManager _cxManager; public ChatApiController(ApplicationDbContext dbContext, - UserManager userManager, + UserManager userManager, IConnexionManager cxManager) { this.dbContext = dbContext; diff --git a/src/Yavsc.Api/Controllers/Relationship/ChatRoomAccessApiController.cs b/src/Yavsc.Api/Controllers/Relationship/ChatRoomAccessApiController.cs index 5fe3a0bf6..fba8bd432 100644 --- a/src/Yavsc.Api/Controllers/Relationship/ChatRoomAccessApiController.cs +++ b/src/Yavsc.Api/Controllers/Relationship/ChatRoomAccessApiController.cs @@ -9,7 +9,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/ChatRoomAccessApi")] + [Route(Constants.APIPrefix + "/ChatRoomAccessApi")] public class ChatRoomAccessApiController : Controller { private readonly ApplicationDbContext _context; @@ -37,7 +37,7 @@ namespace Yavsc.Controllers ChatRoomAccess chatRoomAccess = await _context.ChatRoomAccess.SingleAsync(m => m.ChannelName == id); - + if (chatRoomAccess == null) { @@ -46,13 +46,13 @@ namespace Yavsc.Controllers var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); if (uid != chatRoomAccess.UserId && uid != chatRoomAccess.Room.OwnerId - && ! User.IsInMsRole(YavscConstants.AdminGroupName)) - + && ! User.IsInMsRole(Constants.AdminGroupName)) + { ModelState.AddModelError("UserId","get refused"); return BadRequest(ModelState); } - + return Ok(chatRoomAccess); } @@ -72,7 +72,7 @@ namespace Yavsc.Controllers } var room = _context.ChatRoom.First(channel => channel.Name == chatRoomAccess.ChannelName ); - if (uid != room.OwnerId && ! User.IsInMsRole(YavscConstants.AdminGroupName)) + if (uid != room.OwnerId && ! User.IsInMsRole(Constants.AdminGroupName)) { ModelState.AddModelError("ChannelName", "access put refused"); return BadRequest(ModelState); @@ -110,7 +110,7 @@ namespace Yavsc.Controllers var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); var room = _context.ChatRoom.First(channel => channel.Name == chatRoomAccess.ChannelName ); - if (room == null || (uid != room.OwnerId && ! User.IsInMsRole(YavscConstants.AdminGroupName))) + if (room == null || (uid != room.OwnerId && ! User.IsInMsRole(Constants.AdminGroupName))) { ModelState.AddModelError("ChannelName", "access post refused"); return BadRequest(ModelState); @@ -154,7 +154,7 @@ namespace Yavsc.Controllers var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); var room = _context.ChatRoom.First(channel => channel.Name == chatRoomAccess.ChannelName ); - if (room == null || (uid != room.OwnerId && chatRoomAccess.UserId != uid && ! User.IsInMsRole(YavscConstants.AdminGroupName))) + if (room == null || (uid != room.OwnerId && chatRoomAccess.UserId != uid && ! User.IsInMsRole(Constants.AdminGroupName))) { ModelState.AddModelError("UserId", "access drop refused"); return BadRequest(ModelState); diff --git a/src/Yavsc.Api/Controllers/Relationship/ChatRoomApiController.cs b/src/Yavsc.Api/Controllers/Relationship/ChatRoomApiController.cs index 990646fc8..5d59f6bd1 100644 --- a/src/Yavsc.Api/Controllers/Relationship/ChatRoomApiController.cs +++ b/src/Yavsc.Api/Controllers/Relationship/ChatRoomApiController.cs @@ -8,7 +8,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/ChatRoomApi")] + [Route(Constants.APIPrefix + "/ChatRoomApi")] public class ChatRoomApiController : Controller { private readonly ApplicationDbContext _context; @@ -128,7 +128,7 @@ namespace Yavsc.Controllers } ChatRoom chatRoom = await _context.ChatRoom.SingleAsync(m => m.Name == id); - + if (chatRoom == null) { @@ -137,7 +137,7 @@ namespace Yavsc.Controllers if (User.GetUserId() != chatRoom.OwnerId ) { - if (!User.IsInMsRole(YavscConstants.AdminGroupName)) + if (!User.IsInMsRole(Constants.AdminGroupName)) return BadRequest(new {error = "OwnerId"}); } diff --git a/src/Yavsc.Api/Controllers/Relationship/ContactsApiController.cs b/src/Yavsc.Api/Controllers/Relationship/ContactsApiController.cs index ffd6eb0b6..96ba03dc1 100644 --- a/src/Yavsc.Api/Controllers/Relationship/ContactsApiController.cs +++ b/src/Yavsc.Api/Controllers/Relationship/ContactsApiController.cs @@ -8,7 +8,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/ContactsApi")] + [Route(Constants.APIPrefix + "/ContactsApi")] public class ContactsApiController : Controller { private readonly ApplicationDbContext _context; diff --git a/src/Yavsc.Api/Controllers/ServiceApiController.cs b/src/Yavsc.Api/Controllers/ServiceApiController.cs index e9330543b..8556fb5aa 100644 --- a/src/Yavsc.Api/Controllers/ServiceApiController.cs +++ b/src/Yavsc.Api/Controllers/ServiceApiController.cs @@ -9,7 +9,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/ServiceApi")] + [Route(Constants.APIPrefix + "/ServiceApi")] public class ServiceApiController : Controller { private readonly ApplicationDbContext _context; @@ -46,7 +46,7 @@ namespace Yavsc.Controllers } // PUT: api/ServiceApi/5 - [HttpPut("{id}"),Authorize(YavscConstants.FrontOfficeGroupName)] + [HttpPut("{id}"),Authorize(Constants.FrontOfficeGroupName)] public IActionResult PutService(long id, [FromBody] Service service) { if (!ModelState.IsValid) @@ -81,7 +81,7 @@ namespace Yavsc.Controllers } // POST: api/ServiceApi - [HttpPost,Authorize(YavscConstants.FrontOfficeGroupName)] + [HttpPost,Authorize(Constants.FrontOfficeGroupName)] public IActionResult PostService([FromBody] Service service) { if (!ModelState.IsValid) @@ -110,7 +110,7 @@ namespace Yavsc.Controllers } // DELETE: api/ServiceApi/5 - [HttpDelete("{id}"),Authorize(YavscConstants.FrontOfficeGroupName)] + [HttpDelete("{id}"),Authorize(Constants.FrontOfficeGroupName)] public IActionResult DeleteService(long id) { if (!ModelState.IsValid) diff --git a/src/Yavsc.Api/Controllers/accounting/ApplicationUserApiController.cs b/src/Yavsc.Api/Controllers/accounting/ApplicationUserApiController.cs index 11c70d60b..cb565a0d9 100644 --- a/src/Yavsc.Api/Controllers/accounting/ApplicationUserApiController.cs +++ b/src/Yavsc.Api/Controllers/accounting/ApplicationUserApiController.cs @@ -13,7 +13,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json"),Authorize("AdministratorOnly")] - [Route("api/users")] + [Route(Constants.APIPrefix + "/users")] public class ApplicationUserApiController : Controller { private readonly ApplicationDbContext _context; @@ -28,7 +28,7 @@ namespace Yavsc.Controllers public IEnumerable GetApplicationUser(int skip=0, int take = 25) { return _context.Users.Skip(skip).Take(take) - .Select(u=> new UserInfo{ + .Select(u=> new UserInfo{ UserId = u.Id, UserName = u.UserName, Avatar = u.Avatar}); @@ -39,7 +39,7 @@ namespace Yavsc.Controllers { return _context.Users.Where(u => u.UserName.Contains(pattern)) .Skip(skip).Take(take) - .Select(u=> new UserInfo { + .Select(u=> new UserInfo { UserId = u.Id, UserName = u.UserName, Avatar = u.Avatar }); diff --git a/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs b/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs index 1e610082a..66398c5ae 100644 --- a/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs +++ b/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs @@ -145,7 +145,7 @@ public sealed class BlogsWebServerFixture : WebHostFixture // remaps long Microsoft claim URIs, not sub). // UserHelpers.GetUserId reads sub directly. NameClaimType = "sub", - RoleClaimType = YavscConstants.RoleClaimType, + RoleClaimType = Yavsc.Constants.RoleClaimType, }; }); diff --git a/src/Yavsc.Blogs.Tests/CircleMembersApiTests.cs b/src/Yavsc.Blogs.Tests/CircleMembersApiTests.cs index 5e8040ef4..a2367180f 100644 --- a/src/Yavsc.Blogs.Tests/CircleMembersApiTests.cs +++ b/src/Yavsc.Blogs.Tests/CircleMembersApiTests.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Yavsc.Models; using Yavsc.Models.Relationship; using Yavsc.Tests.Shared; +using static Yavsc.Constants; namespace Yavsc.Blogs.Tests; @@ -88,7 +89,7 @@ public sealed class CircleMembersApiTests : IClassFixture } private string MembersUrl(long circleId) - => $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{Constants.APIPrefix}/circle/{circleId}/members"; + => $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{APIPrefix}/circle/{circleId}/members"; private HttpClient NewClient(string subject) { diff --git a/src/Yavsc.Blogs.Tests/MappedClaimsBlogsWebServerFixture.cs b/src/Yavsc.Blogs.Tests/MappedClaimsBlogsWebServerFixture.cs index c9d957742..6d84aed54 100644 --- a/src/Yavsc.Blogs.Tests/MappedClaimsBlogsWebServerFixture.cs +++ b/src/Yavsc.Blogs.Tests/MappedClaimsBlogsWebServerFixture.cs @@ -65,8 +65,8 @@ public sealed class MappedClaimsBlogsWebServerFixture : IDisposable ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKey = TestTokenIssuer.SigningKey, - RoleClaimType = YavscConstants.RoleClaimType, - NameClaimType = YavscConstants.NameClaimType, + RoleClaimType = Yavsc.Constants.RoleClaimType, + NameClaimType = Yavsc.Constants.NameClaimType, }; }); diff --git a/src/Yavsc.Blogs/Constants.cs b/src/Yavsc.Blogs/Constants.cs index 4dbdfb8bf..3e499da49 100644 --- a/src/Yavsc.Blogs/Constants.cs +++ b/src/Yavsc.Blogs/Constants.cs @@ -5,6 +5,4 @@ public static class Constants public const string AdminRole = "Admin"; public const string ModeratorRole = "Moderator"; public const string UserRole = "User"; - - public const string APIPrefix = "api/v1"; } diff --git a/src/Yavsc.Blogs/Controllers/BlogAclApiController.cs b/src/Yavsc.Blogs/Controllers/BlogAclApiController.cs index aa81f9d5a..94821932f 100644 --- a/src/Yavsc.Blogs/Controllers/BlogAclApiController.cs +++ b/src/Yavsc.Blogs/Controllers/BlogAclApiController.cs @@ -1,15 +1,16 @@ -using System.Linq; + using System.Security.Claims; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Yavsc.Models; using Yavsc.Models.Access; using Yavsc.Server.Helpers; +using static Yavsc.Constants; namespace Yavsc.Blogs.Controllers { [Produces("application/json")] - [Route("api/blogacl")] + [Route(APIPrefix+"/blogacl")] public class BlogAclApiController : Controller { private readonly ApplicationDbContext _context; @@ -24,7 +25,7 @@ namespace Yavsc.Blogs.Controllers /// Blog posts (and therefore their ACLs) are private to their /// author — the API never exposes another user's ACL. /// - // GET: api/blogacl + // GET: api/v1/blogacl [HttpGet] public IEnumerable GetBlogACL() { diff --git a/src/Yavsc.Blogs/Controllers/BlogApiController.cs b/src/Yavsc.Blogs/Controllers/BlogApiController.cs index 76aa777e6..fcd3a3364 100644 --- a/src/Yavsc.Blogs/Controllers/BlogApiController.cs +++ b/src/Yavsc.Blogs/Controllers/BlogApiController.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Yavsc.Blogspot; using Yavsc.Server.Exceptions; using Yavsc.Server.Helpers; -using static Yavsc.Blogs.Constants; +using static Yavsc.Constants; namespace Yavsc.Blogs.Controllers { diff --git a/src/Yavsc.Blogs/Controllers/BlogTagsApiController.cs b/src/Yavsc.Blogs/Controllers/BlogTagsApiController.cs index a5d905ebf..ad6a08937 100644 --- a/src/Yavsc.Blogs/Controllers/BlogTagsApiController.cs +++ b/src/Yavsc.Blogs/Controllers/BlogTagsApiController.cs @@ -1,12 +1,8 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Yavsc.Models; using Yavsc.Models.Blog; -using static Yavsc.Blogs.Constants; +using static Yavsc.Constants; namespace Yavsc.Blogs.Controllers { [Produces("application/json")] diff --git a/src/Yavsc.Blogs/Controllers/CircleApiController.cs b/src/Yavsc.Blogs/Controllers/CircleApiController.cs index 73bbfff94..ea3c981b6 100644 --- a/src/Yavsc.Blogs/Controllers/CircleApiController.cs +++ b/src/Yavsc.Blogs/Controllers/CircleApiController.cs @@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore; using Yavsc.Models; using Yavsc.Models.Relationship; using Yavsc.Server.Helpers; -using static Yavsc.Blogs.Constants; +using static Yavsc.Constants; namespace Yavsc.Blogs.Controllers { diff --git a/src/Yavsc.Blogs/Controllers/CommentsApiController.cs b/src/Yavsc.Blogs/Controllers/CommentsApiController.cs index b9f334dcf..d4c80f996 100644 --- a/src/Yavsc.Blogs/Controllers/CommentsApiController.cs +++ b/src/Yavsc.Blogs/Controllers/CommentsApiController.cs @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore; using Yavsc.Models; using Yavsc.Models.Blog; using Yavsc.Server.Helpers; -using static Yavsc.Blogs.Constants; +using static Yavsc.Constants; namespace Yavsc.Blogs.Controllers { diff --git a/src/Yavsc.Blogs/Controllers/FileSystemApiController.cs b/src/Yavsc.Blogs/Controllers/FileSystemApiController.cs index 5b067c1bc..5e8345034 100644 --- a/src/Yavsc.Blogs/Controllers/FileSystemApiController.cs +++ b/src/Yavsc.Blogs/Controllers/FileSystemApiController.cs @@ -2,7 +2,7 @@ using System.Security.Claims; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using static Yavsc.Blogs.Constants; +using static Yavsc.Constants; namespace Yavsc.Blogs.Controllers { @@ -21,7 +21,7 @@ namespace Yavsc.Blogs.Controllers private readonly ILogger _logger; public FileSystemApiController(ApplicationDbContext context, - IAuthorizationService authorizationService, + IAuthorizationService authorizationService, ILoggerFactory loggerFactory) { @@ -38,7 +38,7 @@ namespace Yavsc.Blogs.Controllers [HttpGet("{*subdir}")] public IActionResult GetDir([ValidRemoteUserFilePath] string subdir="") - { + { if (!ModelState.IsValid) return new BadRequestObjectResult(ModelState); // _logger.LogInformation($"listing files from {User.Identity.Name}{subdir}"); var files = AbstractFileSystemHelpers.GetUserFiles(User.GetUserId(), subdir); @@ -57,20 +57,20 @@ namespace Yavsc.Blogs.Controllers } catch (InvalidPathException ex) { pathex = ex; } - if (pathex!=null) + if (pathex!=null) { _logger.LogError($"invalid sub path: '{subdir}'."); return BadRequest(pathex); } _logger.LogInformation($"Receiving files, saved in '{destDir}' (specified as '{subdir}')."); - + var uid = User.GetUserId(); var user = dbContext.Users.Single( u => u.Id == uid ); int i=0; _logger.LogInformation($"Receiving {Request.Form.Files.Count} files."); - + foreach (var f in Request.Form.Files) { var item = user.ReceiveUserFile(destDir, f); @@ -178,7 +178,7 @@ namespace Yavsc.Blogs.Controllers return Ok(new { deleted=id }); } - + } } diff --git a/src/Yavsc.Blogs/Controllers/FileSystemStreamController.cs b/src/Yavsc.Blogs/Controllers/FileSystemStreamController.cs index 23cf0cc60..bc6485dd4 100644 --- a/src/Yavsc.Blogs/Controllers/FileSystemStreamController.cs +++ b/src/Yavsc.Blogs/Controllers/FileSystemStreamController.cs @@ -8,7 +8,7 @@ using Yavsc.Models.Messaging; using Yavsc.Services; using Microsoft.AspNetCore.SignalR; using Yavsc.Server.Helpers; -using static Yavsc.Blogs.Constants; +using static Yavsc.Constants; using Yavsc.Server.Hubs; namespace Yavsc.Blogs.Controllers diff --git a/src/Yavsc.Blogs/Controllers/PostTagsApiController.cs b/src/Yavsc.Blogs/Controllers/PostTagsApiController.cs index e908edec6..da03c19c6 100644 --- a/src/Yavsc.Blogs/Controllers/PostTagsApiController.cs +++ b/src/Yavsc.Blogs/Controllers/PostTagsApiController.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Mvc; -using static Yavsc.Blogs.Constants; +using static Yavsc.Constants; namespace Yavsc.Blogs.Controllers { diff --git a/src/Yavsc.Blogs/Controllers/TagsApiController.cs b/src/Yavsc.Blogs/Controllers/TagsApiController.cs index daf0220b2..d4c2b5389 100644 --- a/src/Yavsc.Blogs/Controllers/TagsApiController.cs +++ b/src/Yavsc.Blogs/Controllers/TagsApiController.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Yavsc.Models; -using static Yavsc.Blogs.Constants; +using static Yavsc.Constants; namespace Yavsc.Controllers { diff --git a/src/Yavsc.Blogs/Controllers/UserSearchApiController.cs b/src/Yavsc.Blogs/Controllers/UserSearchApiController.cs index 048db1543..951a5880e 100644 --- a/src/Yavsc.Blogs/Controllers/UserSearchApiController.cs +++ b/src/Yavsc.Blogs/Controllers/UserSearchApiController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Yavsc.Models; +using static Yavsc.Constants; namespace Yavsc.Blogs.Controllers { @@ -26,7 +27,7 @@ namespace Yavsc.Blogs.Controllers /// exposing it. /// [Produces("application/json")] - [Route( Constants.APIPrefix + "/user-search")] + [Route(APIPrefix + "/user-search")] [Authorize] public class UserSearchApiController : Controller { @@ -66,8 +67,9 @@ namespace Yavsc.Blogs.Controllers // book callers already know the email they're // searching for and we don't want to surface a // long tail of partial matches. - var normalised = e.Trim(); - query = query.Where(u => u.Email != null && u.Email.ToLower() == normalised.ToLower()); + var normalized = e.Trim(); + query = query.Where(u => u.Email != null && + string.Compare(u.Email, normalized, true) ==0); } if (!string.IsNullOrWhiteSpace(q)) diff --git a/src/Yavsc.Blogs/Program.cs b/src/Yavsc.Blogs/Program.cs index 742c9eeee..952115d33 100644 --- a/src/Yavsc.Blogs/Program.cs +++ b/src/Yavsc.Blogs/Program.cs @@ -51,7 +51,7 @@ internal class Program // DbContextBuilder services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString( - YavscConstants.YavscConnectionStringName))); + Yavsc.Constants.YavscConnectionStringName))); // other services services diff --git a/src/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs b/src/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs index a0249fa45..7543a42e7 100644 --- a/src/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs +++ b/src/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs @@ -14,7 +14,7 @@ namespace Yavsc.Org.Tests.NonRegression; /// ne voit rien — juste un 500 muet. /// /// Le fix passe par qui -/// retourne pour toute +/// retourne pour toute /// donnée partielle. Ces tests couvrent les trois formes de /// "donnée absente" : user null, UserName vide, UserName whitespace. /// @@ -23,21 +23,21 @@ public class UserDisplayHelpersTests [Fact] public void AvatarSrc_null_user_returns_default_avatar() { - Assert.Equal(YavscConstants.DefaultAvatar, UserDisplayHelpers.AvatarSrc(null)); + Assert.Equal(Yavsc.Constants.DefaultAvatar, UserDisplayHelpers.AvatarSrc(null)); } [Fact] public void AvatarSrc_user_with_empty_UserName_returns_default_avatar() { var user = new FakeUser { UserName = "" }; - Assert.Equal(YavscConstants.DefaultAvatar, UserDisplayHelpers.AvatarSrc(user)); + Assert.Equal(Yavsc.Constants.DefaultAvatar, UserDisplayHelpers.AvatarSrc(user)); } [Fact] public void AvatarSrc_user_with_whitespace_UserName_returns_default_avatar() { var user = new FakeUser { UserName = " " }; - Assert.Equal(YavscConstants.DefaultAvatar, UserDisplayHelpers.AvatarSrc(user)); + Assert.Equal(Yavsc.Constants.DefaultAvatar, UserDisplayHelpers.AvatarSrc(user)); } [Fact] @@ -47,7 +47,7 @@ public class UserDisplayHelpersTests // Le path doit matcher YavscConstants.AvatarsPath (minuscule), // pas un /Avatars/ avec S majuscule qui ne résout pas // dans le middleware de fichiers statiques. - var expected = $"{YavscConstants.AvatarsPath}/alice.s.png"; + var expected = $"{Yavsc.Constants.AvatarsPath}/alice.s.png"; Assert.Equal(expected, UserDisplayHelpers.AvatarSrc(user)); } diff --git a/src/Yavsc.Org.Tests/WebServerFixture.cs b/src/Yavsc.Org.Tests/WebServerFixture.cs index a623bc9e4..edfd87d9b 100644 --- a/src/Yavsc.Org.Tests/WebServerFixture.cs +++ b/src/Yavsc.Org.Tests/WebServerFixture.cs @@ -80,8 +80,8 @@ public sealed class WebServerFixture : WebHostFixture // can resolve it. The AddConfiguration extension takes care of // that plus the in-memory overrides below. builder.AddConfiguration(null).AddInMemoryCollection(new Dictionary - { - [$"ConnectionStrings:{YavscConstants.YavscConnectionStringName}"] = "InMemory", + { + [$"ConnectionStrings:{Yavsc.Constants.YavscConnectionStringName}"] = "InMemory", // SMTP test config: UserName non-null so MailSender // exercises the Authenticate branch — the // RecordingSmtpClient captures it. diff --git a/src/Yavsc.Org/Controllers/Accounting/AccountController.cs b/src/Yavsc.Org/Controllers/Accounting/AccountController.cs index 435654429..c562736df 100644 --- a/src/Yavsc.Org/Controllers/Accounting/AccountController.cs +++ b/src/Yavsc.Org/Controllers/Accounting/AccountController.cs @@ -90,7 +90,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, "ConfirmYourAccountTitle" }) Debug.Assert(!_localizer[name].ResourceNotFound); - + } @@ -116,7 +116,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, { await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName, clientId: context?.Client.ClientId)); - // only set explicit expiration here if user chooses "remember me". + // only set explicit expiration here if user chooses "remember me". // otherwise we rely upon expiration configured in cookie middleware. await HttpContext.SignInAsync(user, _roleManager, model.RememberMe, _dbContext); var authResult = await HttpContext.AuthenticateAsync(); @@ -198,7 +198,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, /// /// Entry point into the login workflow /// - [HttpGet(YavscConstants.SigninPath)] + [HttpGet(Constants.SigninPath)] public async Task Signin(SignInModel model) { // build a model so we know what to show on the login page @@ -216,11 +216,11 @@ IHtmlLocalizerFactory htmlLocalizerFactory, /// /// Handle postback from username/password login /// - /// - [HttpPost(YavscConstants.SigninPath)] + /// + [HttpPost(Constants.SigninPath)] [ValidateAntiForgeryToken] [AllowAnonymous] - + public async Task Signin([FromForm] SignInModel model, [FromForm] string button) { @@ -232,7 +232,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, { if (context != null) { - // if the user cancels, send a result back into IdentityServer as if they + // if the user cancels, send a result back into IdentityServer as if they // denied the consent (even if this client does not require consent). // this will send back an access denied OIDC error response to the client. await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied); @@ -269,7 +269,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, { await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName, clientId: context?.Client.ClientId)); - // only set explicit expiration here if user chooses "remember me". + // only set explicit expiration here if user chooses "remember me". // otherwise we rely upon expiration configured in cookie middleware. await HttpContext.SignInAsync(user, _roleManager, model.RememberMe, _dbContext); @@ -396,7 +396,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, var local = context.IdP == IdentityServer8.IdentityServerConstants.LocalIdentityProvider; // this is meant to short circuit the UI and only trigger the one external IdP - + model.EnableLocalLogin = local; model.UserName = context?.LoginHint; model.IsExternalLoginOnly = false; @@ -579,7 +579,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, // Send an email with this link Uri authority = new Uri(Config.Authority); - + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code }, @@ -659,7 +659,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, } // // POST: /Account/LogOff - [HttpPost(YavscConstants.LogoutPath)] + [HttpPost(Constants.LogoutPath)] [ValidateAntiForgeryToken] public async Task LogOff(string returnUrl = null) { @@ -829,7 +829,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, bool result = false; try { - result = await _userManager.VerifyTwoFactorTokenAsync(user, YavscConstants.DefaultFactor, code); + result = await _userManager.VerifyTwoFactorTokenAsync(user, Constants.DefaultFactor, code); _dbContext.SaveChanges(userId); } catch (Exception ex) @@ -1024,12 +1024,12 @@ IHtmlLocalizerFactory htmlLocalizerFactory, } // Generate the token and send it - if (model.SelectedProvider == YavscConstants.MobileAppFactor) + if (model.SelectedProvider == Constants.MobileAppFactor) { return View("Error", new Exception("No mobile app service was activated")); } else - if (model.SelectedProvider == YavscConstants.SMSFactor) + if (model.SelectedProvider == Constants.SMSFactor) { return View("Error", new Exception("No SMS service was activated")); // await _smsSender.SendSmsAsync(_twilioSettings, await _userManager.GetPhoneNumberAsync(user), message); diff --git a/src/Yavsc.Org/Controllers/Administration/AdministrationController.cs b/src/Yavsc.Org/Controllers/Administration/AdministrationController.cs index 7104e178b..e2944e9e0 100644 --- a/src/Yavsc.Org/Controllers/Administration/AdministrationController.cs +++ b/src/Yavsc.Org/Controllers/Administration/AdministrationController.cs @@ -50,12 +50,12 @@ namespace Yavsc.Controllers { // ensure all roles existence foreach (string roleName in new string[] { - YavscConstants.AdminGroupName, - YavscConstants.StarGroupName, - YavscConstants.PerformerGroupName, - YavscConstants.FrontOfficeGroupName, - YavscConstants.StarHunterGroupName, - YavscConstants.BlogModeratorGroupName + Constants.AdminGroupName, + Constants.StarGroupName, + Constants.PerformerGroupName, + Constants.FrontOfficeGroupName, + Constants.StarHunterGroupName, + Constants.BlogModeratorGroupName }) if (!await _roleManager.RoleExistsAsync(roleName)) { @@ -80,11 +80,11 @@ namespace Yavsc.Controllers public async Task Take() { // If some amdin already exists, make this method disapear - var admins = await _userManager.GetUsersInRoleAsync(YavscConstants.AdminGroupName); + var admins = await _userManager.GetUsersInRoleAsync(Constants.AdminGroupName); if (admins != null && admins.Count > 0) { // All is ok, nothing to do here. - if (User.IsInMsRole(YavscConstants.AdminGroupName)) + if (User.IsInMsRole(Constants.AdminGroupName)) { return Ok(new { message = "you already got it." }); @@ -100,7 +100,7 @@ namespace Yavsc.Controllers return new BadRequestObjectResult(ModelState); } - var addToRoleResult = await _userManager.AddToRoleAsync(user, YavscConstants.AdminGroupName); + var addToRoleResult = await _userManager.AddToRoleAsync(user, Constants.AdminGroupName); if (!addToRoleResult.Succeeded) { AddErrors(addToRoleResult); @@ -114,11 +114,11 @@ namespace Yavsc.Controllers public async Task Index() { var adminCount = await _userManager.GetUsersInRoleAsync( - YavscConstants.AdminGroupName); + Constants.AdminGroupName); var userCount = await _dbContext.Users.CountAsync(); var youAreAdmin = await _userManager.IsInRoleAsync( await _userManager.FindByIdAsync(User.GetUserId()), - YavscConstants.AdminGroupName); + Constants.AdminGroupName); var roles = await _roleManager.Roles.Select(x => new RoleInfo { diff --git a/src/Yavsc.Org/Controllers/Administration/ApiScopesApiController.cs b/src/Yavsc.Org/Controllers/Administration/ApiScopesApiController.cs index 983e7233e..d6e83c364 100644 --- a/src/Yavsc.Org/Controllers/Administration/ApiScopesApiController.cs +++ b/src/Yavsc.Org/Controllers/Administration/ApiScopesApiController.cs @@ -1,13 +1,13 @@ using IdentityServer8.EntityFramework.Entities; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Yavsc.Models; using Yavsc.Server.Helpers; +using static Yavsc.Constants; namespace Yavsc.Org.Controllers.Administration { - [Route("api/[controller]")] + [Route(APIPrefix + "/[controller]")] [ApiController] public class ApiScopesApiController : ControllerBase { diff --git a/src/Yavsc.Org/Controllers/Communicating/AnnouncesController.cs b/src/Yavsc.Org/Controllers/Communicating/AnnouncesController.cs index e5002168e..f8b41c99a 100644 --- a/src/Yavsc.Org/Controllers/Communicating/AnnouncesController.cs +++ b/src/Yavsc.Org/Controllers/Communicating/AnnouncesController.cs @@ -18,11 +18,11 @@ namespace Yavsc.Controllers readonly IStringLocalizer _localizer; readonly IAuthorizationService _authorizationService; - public AnnouncesController(ApplicationDbContext context, + public AnnouncesController(ApplicationDbContext context, IAuthorizationService authorizationService, IStringLocalizer localizer) { - _context = context; + _context = context; _authorizationService = authorizationService; _localizer = localizer; } @@ -59,16 +59,16 @@ namespace Yavsc.Controllers } private async Task SetupView(Announce announce) { - ViewBag.IsAdmin = User.IsInMsRole(YavscConstants.AdminGroupName); - ViewBag.IsPerformer = User.IsInMsRole(YavscConstants.PerformerGroupName); + ViewBag.IsAdmin = User.IsInMsRole(Constants.AdminGroupName); + ViewBag.IsPerformer = User.IsInMsRole(Constants.PerformerGroupName); ViewBag.AllowEdit = announce==null || announce.Id<=0 || !_authorizationService.AuthorizeAsync(User,announce,new EditPermission()).IsFaulted; List dl = new List(); var rnames = System.Enum.GetNames(typeof(Reason)); var rvalues = System.Enum.GetValues(typeof(Reason)); - + for (int i = 0; i a.Children).FirstOrDefault(a => a.Code == code); @@ -123,9 +123,9 @@ namespace Yavsc.Controllers [ValidateAntiForgeryToken] public IActionResult Create(Activity activity) { - if (activity.ParentCode==YavscConstants.NoneCode) + if (activity.ParentCode==Constants.NoneCode) activity.ParentCode=null; - if (activity.SettingsClassName==YavscConstants.NoneCode) + if (activity.SettingsClassName==Constants.NoneCode) activity.SettingsClassName=null; if (ModelState.IsValid) @@ -161,9 +161,9 @@ namespace Yavsc.Controllers [ValidateAntiForgeryToken] public IActionResult Edit(Activity activity) { - if (activity.ParentCode==YavscConstants.NoneCode) + if (activity.ParentCode==Constants.NoneCode) activity.ParentCode=null; - if (activity.SettingsClassName==YavscConstants.NoneCode) + if (activity.SettingsClassName==Constants.NoneCode) activity.SettingsClassName=null; if (ModelState.IsValid) { diff --git a/src/Yavsc.Org/Controllers/DimissClicksApiController.cs b/src/Yavsc.Org/Controllers/DimissClicksApiController.cs index 19f90787d..b07bc4b3b 100644 --- a/src/Yavsc.Org/Controllers/DimissClicksApiController.cs +++ b/src/Yavsc.Org/Controllers/DimissClicksApiController.cs @@ -10,7 +10,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/v1/dimiss")] + [Route(Constants.APIPrefix + "/v1/dimiss")] public class DimissClicksApiController : Controller { private readonly ApplicationDbContext _context; @@ -140,7 +140,7 @@ namespace Yavsc.Controllers var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); if (!User.IsInRole("Administrator")) if (uid != id) return new ChallengeResult(); - + if (!ModelState.IsValid) { return BadRequest(ModelState); diff --git a/src/Yavsc.Org/Controllers/HomeController.cs b/src/Yavsc.Org/Controllers/HomeController.cs index 67c19fed8..c7aa131ff 100644 --- a/src/Yavsc.Org/Controllers/HomeController.cs +++ b/src/Yavsc.Org/Controllers/HomeController.cs @@ -20,10 +20,10 @@ namespace Yavsc.Controllers readonly IHtmlLocalizer _localizer; private SiteSettings siteSettings; - public HomeController(ILogger logger, - IHtmlLocalizer localizer, + public HomeController(ILogger logger, + IHtmlLocalizer localizer, ApplicationDbContext context, - IOptions settingsOptions, + IOptions settingsOptions, IWebHostEnvironment env ) { @@ -37,9 +37,9 @@ namespace Yavsc.Controllers public async Task Index(string id) { - ViewBag.IsFromSecureProx = Request.Headers.ContainsKey(YavscConstants.SshHeaderKey) && Request.Headers[YavscConstants.SshHeaderKey] == "on"; + ViewBag.IsFromSecureProx = Request.Headers.ContainsKey(Constants.SshHeaderKey) && Request.Headers[Constants.SshHeaderKey] == "on"; ViewBag.SecureHomeUrl = "https://" + Request.Headers["X-Forwarded-Host"]; - ViewBag.SshHeaderKey = Request.Headers[YavscConstants.SshHeaderKey]; + ViewBag.SshHeaderKey = Request.Headers[Constants.SshHeaderKey]; var uid = User.GetUserId(); long[] clicked = null; if (uid == null) @@ -140,8 +140,8 @@ namespace Yavsc.Controllers errorViewModel.Description ??= string.Empty; errorViewModel.Description += " Page: Home."; } - - + + return View("~/Views/Shared/Error.cshtml", errorViewModel); } public IActionResult Status(int id) diff --git a/src/Yavsc.Org/Controllers/Musical/InstrumentationController.cs b/src/Yavsc.Org/Controllers/Musical/InstrumentationController.cs index dc905d08b..536c7417b 100644 --- a/src/Yavsc.Org/Controllers/Musical/InstrumentationController.cs +++ b/src/Yavsc.Org/Controllers/Musical/InstrumentationController.cs @@ -17,7 +17,7 @@ namespace Yavsc.Controllers public InstrumentationController(ApplicationDbContext context) { - _context = context; + _context = context; } // GET: Instrumentation @@ -50,7 +50,7 @@ namespace Yavsc.Controllers var owned = _context.Instrumentation.Include(i=>i.Tool).Where(i=>i.UserId==uid).Select(i=>i.InstrumentId); var ownedArray = owned.ToArray(); - ViewBag.YetAvailableInstruments = _context.Instrument.Select(k=>new SelectListItem + ViewBag.YetAvailableInstruments = _context.Instrument.Select(k=>new SelectListItem { Text = k.Name, Value = k.Id.ToString(), Disabled = ownedArray.Contains(k.Id) }); return View(new Instrumentation { UserId = uid }); @@ -64,7 +64,7 @@ namespace Yavsc.Controllers var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); if (ModelState.IsValid) { - if (model.UserId != uid) if (!User.IsInMsRole(YavscConstants.AdminGroupName)) + if (model.UserId != uid) if (!User.IsInMsRole(Constants.AdminGroupName)) return new ChallengeResult(); _context.Instrumentation.Add(model); @@ -82,7 +82,7 @@ namespace Yavsc.Controllers { return NotFound(); } - if (id != uid) if (!User.IsInMsRole(YavscConstants.AdminGroupName)) + if (id != uid) if (!User.IsInMsRole(Constants.AdminGroupName)) return new ChallengeResult(); Instrumentation musicianSettings = await _context.Instrumentation.SingleAsync(m => m.UserId == id); if (musicianSettings == null) @@ -98,7 +98,7 @@ namespace Yavsc.Controllers public async Task Edit(Instrumentation musicianSettings) { var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (musicianSettings.UserId != uid) if (!User.IsInMsRole(YavscConstants.AdminGroupName)) + if (musicianSettings.UserId != uid) if (!User.IsInMsRole(Constants.AdminGroupName)) return new ChallengeResult(); if (ModelState.IsValid) { @@ -124,7 +124,7 @@ namespace Yavsc.Controllers return NotFound(); } var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (musicianSettings.UserId != uid) if (!User.IsInMsRole(YavscConstants.AdminGroupName)) + if (musicianSettings.UserId != uid) if (!User.IsInMsRole(Constants.AdminGroupName)) return new ChallengeResult(); return View(musicianSettings); } @@ -135,12 +135,12 @@ namespace Yavsc.Controllers public async Task DeleteConfirmed(string id) { Instrumentation musicianSettings = await _context.Instrumentation.SingleAsync(m => m.UserId == id); - + var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (musicianSettings.UserId != uid) if (!User.IsInMsRole(YavscConstants.AdminGroupName)) + if (musicianSettings.UserId != uid) if (!User.IsInMsRole(Constants.AdminGroupName)) return new ChallengeResult(); - + _context.Instrumentation.Remove(musicianSettings); await _context.SaveChangesAsync(User.GetUserId()); return RedirectToAction("Index"); diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs index 0cae23a72..f92dc3c9b 100644 --- a/src/Yavsc.Org/Extensions/HostingExtensions.cs +++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs @@ -169,7 +169,7 @@ public static class HostingExtensions public static IdentityBuilder AddIdentityDBAndStores(this WebApplicationBuilder builder) { IServiceCollection services = builder.Services; - var connectionString = builder.Configuration.GetConnectionString(YavscConstants.YavscConnectionStringName); + var connectionString = builder.Configuration.GetConnectionString(Constants.YavscConnectionStringName); services.AddDbContext(options => { @@ -197,7 +197,7 @@ public static class HostingExtensions options.SignIn.RequireConfirmedAccount = builder.Environment.IsEnvironment( builder.Environment.EnvironmentName); options.ClaimsIdentity.UserNameClaimType = JwtClaimTypes.PreferredUserName; - options.ClaimsIdentity.RoleClaimType = YavscConstants.RoleClaimType; + options.ClaimsIdentity.RoleClaimType = Constants.RoleClaimType; } ) .AddEntityFrameworkStores(); @@ -239,18 +239,18 @@ public static class HostingExtensions { policy .RequireAuthenticatedUser() - .RequireClaim(YavscConstants.RoleClaimType, - new string[] { YavscConstants.PerformerGroupName, YavscConstants.AdminGroupName }) + .RequireClaim(Constants.RoleClaimType, + new string[] { Constants.PerformerGroupName, Constants.AdminGroupName }) ; }); options.AddPolicy("AdministratorOnly", policy => { _ = policy .RequireAuthenticatedUser() - .RequireClaim(YavscConstants.RoleClaimType, YavscConstants.AdminGroupName); + .RequireClaim(Constants.RoleClaimType, Constants.AdminGroupName); }); - options.AddPolicy("FrontOffice", policy => policy.RequireRole(YavscConstants.FrontOfficeGroupName)); + options.AddPolicy("FrontOffice", policy => policy.RequireRole(Constants.FrontOfficeGroupName)); // options.AddPolicy("EmployeeId", policy => policy.RequireClaim("EmployeeId", "123", "456")); // options.AddPolicy("BuildingEntry", policy => policy.Requirements.Add(new OfficeEntryRequirement())); @@ -314,10 +314,10 @@ public static class HostingExtensions { options.ClaimsIdentity.UserIdClaimType = JwtClaimTypes.Subject; options.ClaimsIdentity.UserNameClaimType = JwtClaimTypes.Name; - options.ClaimsIdentity.RoleClaimType = YavscConstants.RoleClaimType; + options.ClaimsIdentity.RoleClaimType = Constants.RoleClaimType; }); var migrationsAssembly = typeof(Program).GetTypeInfo().Assembly.GetName().Name; - var connectionString = builder.Configuration.GetConnectionString(YavscConstants.YavscConnectionStringName); + var connectionString = builder.Configuration.GetConnectionString(Constants.YavscConnectionStringName); string sqliteConnectionString = $"Data Source={Path.Combine(Path.GetTempPath(), "yavsc_test.db")}"; @@ -1220,7 +1220,7 @@ ADD COLUMN IF NOT EXISTS ""Moderated"" boolean NOT NULL DEFAULT FALSE;"); Config.UserFilesOptions = new FileServerOptions() { FileProvider = new PhysicalFileProvider(AbstractFileSystemHelpers.UserFilesDirName), - RequestPath = PathString.FromUriComponent(YavscConstants.UserFilesPath), + RequestPath = PathString.FromUriComponent(Constants.UserFilesPath), EnableDirectoryBrowsing = enableDirectoryBrowsing, }; Config.UserFilesOptions.EnableDefaultFiles = true; @@ -1233,7 +1233,7 @@ ADD COLUMN IF NOT EXISTS ""Moderated"" boolean NOT NULL DEFAULT FALSE;"); Config.AvatarsOptions = new FileServerOptions() { FileProvider = new PhysicalFileProvider(Config.AvatarsDirName), - RequestPath = PathString.FromUriComponent(YavscConstants.AvatarsPath), + RequestPath = PathString.FromUriComponent(Constants.AvatarsPath), EnableDirectoryBrowsing = enableDirectoryBrowsing }; @@ -1244,7 +1244,7 @@ ADD COLUMN IF NOT EXISTS ""Moderated"" boolean NOT NULL DEFAULT FALSE;"); Config.GitOptions = new FileServerOptions() { FileProvider = new PhysicalFileProvider(Config.GitDirName), - RequestPath = PathString.FromUriComponent(YavscConstants.GitPath), + RequestPath = PathString.FromUriComponent(Constants.GitPath), EnableDirectoryBrowsing = enableDirectoryBrowsing, }; Config.GitOptions.DefaultFilesOptions.DefaultFileNames.Add("index.md"); diff --git a/src/Yavsc.Org/ViewModels/Manage/SetUserNameViewModel.cs b/src/Yavsc.Org/ViewModels/Manage/SetUserNameViewModel.cs index 69507b92f..61cf07278 100644 --- a/src/Yavsc.Org/ViewModels/Manage/SetUserNameViewModel.cs +++ b/src/Yavsc.Org/ViewModels/Manage/SetUserNameViewModel.cs @@ -7,7 +7,7 @@ namespace Yavsc.ViewModels.Manage public class SetUserNameViewModel { [Required] - [Display(Name = "User name"),RegularExpression(YavscConstants.UserNameRegExp)] + [Display(Name = "User name"),RegularExpression(Constants.UserNameRegExp)] public string UserName { get; set; } } diff --git a/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml b/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml index f0a961c96..dd6b0ff8c 100644 --- a/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml +++ b/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml @@ -13,7 +13,7 @@ } else {
Utilisateur inconnu - Utilisateur inconnu + Utilisateur inconnu
} diff --git a/src/Yavsc.Org/Views/Shared/_LoginPartial.cshtml b/src/Yavsc.Org/Views/Shared/_LoginPartial.cshtml index 431fe61ea..7bfa2a3f9 100644 --- a/src/Yavsc.Org/Views/Shared/_LoginPartial.cshtml +++ b/src/Yavsc.Org/Views/Shared/_LoginPartial.cshtml @@ -16,7 +16,7 @@
  • Features
  • - @if (User.IsInMsRole(YavscConstants.AdminGroupName)) { + @if (User.IsInMsRole(Constants.AdminGroupName)) {