diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml
index ea58d2fef..2f14e449e 100644
--- a/.forgejo/workflows/buildAndTest.yml
+++ b/.forgejo/workflows/buildAndTest.yml
@@ -48,4 +48,4 @@ jobs:
--verbosity normal \
--filter="Category!=Platform-Android" \
--logger "xunit;LogFileName=test-results.xml" \
- && echo "✅ Success !" || echo "❌ Fail ($?)!"
+ && echo "✅ Success !" || { echo "❌ Fail ($?)!"; exit 1; }
diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml
index a72f92bd6..911fc831d 100644
--- a/.forgejo/workflows/release.yml
+++ b/.forgejo/workflows/release.yml
@@ -175,6 +175,12 @@ jobs:
run: |
cd /src/_src
dotnet restore
+ - name: Test
+ run: |
+ cd /src/_src && dotnet test \
+ --verbosity normal \
+ --filter="Category!=Platform-Android" \
+ --logger "xunit;LogFileName=test-results.xml"
- name: Build de PostIt.Android ARM64
run: |
@@ -200,6 +206,7 @@ jobs:
RELEASE_BODY: ${{ env.RELEASE_BODY }}
IS_PRERELEASE: ${{ env.IS_PRERELEASE }}
run: |
+ set -e
if [[ -z "$TAG" ]]; then
echo "::error::No tag resolved for the API call."
exit 1
diff --git a/.gitignore b/.gitignore
index b7813f604..056cf69d2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,6 +35,7 @@ appsettings-*.*.json
generated/
*.tmp
+tmp/
DataDir/
*.tests.trx
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c7c92595..111c1491a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,58 @@
# Changelog
+## [1.0.8-rc14] - unstable
+
+### Added
+
+* [PostIt] Ajout d'un `BillingQueryDetailsPageViewModel` et de sa page associee pour afficher le detail d'une commande billing depuis l'historique.
+* [PostIt] Ajout d'un mode detail avec section metier (statut, date, description, motif, infos) et section technique repliable (code, client, provision, lieu, prestations).
+* [PostIt] Ajout d'un badge de statut enrichi (couleur + pictogramme) sur le detail d'une commande pour visualiser l'etat en un coup d'oeil.
+* [PostIt] Ajout d'un bloc d'actions rapide en tete du detail (`Retour`, `Ouvrir en edition`) pour eviter le scroll jusqu'au bas de page.
+* [PostIt] Ajout d'un style monospace sur les metadonnees techniques (code billing, client, provision, lieu, prestations) pour faciliter la lecture des identifiants et valeurs brutes.
+
+### Changed
+
+* [PostIt] Le bouton d'ouverture depuis la liste billing ouvre maintenant une page de detail dediee avant l'eventuelle edition.
+* [PostIt] Amelioration UX des pages billing: badges de statut colores, actions remontees en haut de page, et typographie monospace sur les metadonnees techniques.
+
+### Fixed
+
+* [Yavsc.Api] Correction d'un 500 sur le refresh du catalogue d'activites lorsque `Activity.Description` est `NULL` en base (nullabilite explicite + projection null-safe + gardes sur codes vides).
+* [Yavsc.Api] Correction des erreurs 400/500 sur les routes billing (`Rdv`, `Brush`, `MBrush`) en imposant `ClientId` depuis l'utilisateur authentifie et en ignorant les champs server-owned lors de la validation modele.
+* [Yavsc.Api] Correction du `PUT /api/v1/billing/Rdv/{id}`: mise a jour controlee de l'entite existante (et non remplacement brut du graphe JSON), ce qui supprime les `BadRequest` parasites.
+* [Yavsc.Api] Correction PostgreSQL `timestamptz` sur RDV: normalisation UTC de `EventDate` sur `POST/PUT /api/v1/billing/Rdv` pour eviter l'erreur `Cannot write DateTime with Kind=Local`.
+* [Yavsc.Api] Correction du flux FrontOffice accept/reject de query: sauvegarde avec contexte utilisateur et fallback d'injection pour `IBillingService` afin d'eviter les erreurs serveur en environnement de test.
+* [Yavsc.Blogs] Correction des `BadRequest` sur `POST/PUT /api/v1/blogspot` avec payload JSON (PostIt): les proprietes de navigation/serveur (`Author`, `Tags`, `Comments`, audit) ne bloquent plus la validation.
+* [Yavsc.Org] Correction du flux MVC de creation de commentaire: `SaveChangesAsync(userId)` est utilise pour renseigner les champs d'audit requis (`UserCreated`/`UserModified`).
+* [Yavsc.Api.Test] Stabilisation des fixtures de seed billing: remplissage des metadonnees d'audit (`UserCreated`, `UserModified`, dates) pour eviter les echecs SQLite `NOT NULL`.
+
+## [1.0.8-rc13] - unstable
+
+### Added
+
+* [PostIt] Integration d'un selecteur de lieu RDV base sur Mapsui (carte interactive dans le formulaire `Rdv`).
+* [PostIt] Ajout d'un marqueur de position et d'une action de recentrage sur la carte RDV.
+* [PostIt] Ajout d'un service de reverse geocoding pour suggerer une adresse a partir des coordonnees carte.
+* [PostIt] Cache et debounce des resolutions d'adresse RDV pour limiter les appels reseau et lisser l'UX.
+* [PostIt.Tests] Nouvelles non-regressions sur le panneau d'adresse suggeree RDV et le comportement de la carte.
+* [Yavsc.Abstract] Activation de `#nullable enable annotations` sur les fichiers legacy avec annotations nullable.
+* [Yavsc.Server] Activation de `#nullable enable annotations` sur les fichiers legacy avec annotations nullable.
+
+### Changed
+
+* [PostIt] Generalisation de la barre de statut d'action (severite explicite) sur pages principales, dialogues et formulaires billing.
+* [PostIt] Harmonisation des messages de statut utilisateur en francais.
+* [PostIt] Renforcement des gardes de navigation dans les flux de gestion des membres de cercle.
+* [PostIt] Le flux RDV conserve l'adresse saisie manuellement et propose l'adresse resolue comme suggestion explicite.
+* [PostIt] Le flux de geolocalisation RDV tolere les positions proches dans le cache de suggestion d'adresse.
+
+### Fixed
+
+* [PostIt.Desktop] Correction d'un crash au demarrage OIDC (`No authority specified`) via durcissement des valeurs par defaut de configuration d'authentification.
+* [PostIt] Correction de la persistance des settings: l'etat runtime de statut n'est plus serialize dans le JSON utilisateur.
+* [PostIt.Tests] Ajout d'un verrou de non-regression sur le premier chargement des settings.
+* [PostIt] Correction du binding de la date RDV: `DatePicker.SelectedDate` est aligne sur un proxy `DateTimeOffset?` (`EventDateSelection`).
+
## [1.0.8-rc12] - unstable
### Added
diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props
index 4dd4b2889..2f27340e9 100644
--- a/src/PostIt/Directory.Packages.props
+++ b/src/PostIt/Directory.Packages.props
@@ -16,6 +16,8 @@
+
+
diff --git a/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs b/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs
index c7b5b926b..17fcc968c 100644
--- a/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs
+++ b/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs
@@ -115,6 +115,89 @@ public class BillingCommandPageViewModelTests
}
}
+ [Fact]
+ public void ApplyLocationFromMap_sets_coordinates_and_updates_status()
+ {
+ var api = new RecordingApi();
+ var client = new BillingApiClient(api, "https://business.example/api/v1/");
+ var vm =
+ new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" }
+ .CreateCommandPageViewModel(
+ new ActivityInfo { Code = "dev", Name = "Développement" },
+ new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" },
+ client) as RdvViewModel;
+
+ vm!.Address = string.Empty;
+ vm.ApplyLocationFromMap(48.85661234, 2.35224567);
+
+ Assert.Equal(48.856612, vm.Latitude);
+ Assert.Equal(2.352246, vm.Longitude);
+ Assert.Contains("Position sélectionnée", vm.StatusMessage, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void EventDateSelection_round_trips_with_EventDate_for_DatePicker_binding()
+ {
+ var api = new RecordingApi();
+ var client = new BillingApiClient(api, "https://business.example/api/v1/");
+ var vm =
+ new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" }
+ .CreateCommandPageViewModel(
+ new ActivityInfo { Code = "dev", Name = "Développement" },
+ new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" },
+ client) as RdvViewModel;
+
+ var selected = new DateTimeOffset(2026, 9, 7, 14, 30, 0, TimeSpan.FromHours(2));
+ vm!.EventDateSelection = selected;
+
+ Assert.Equal(selected.LocalDateTime, vm.EventDate);
+ Assert.Equal(vm.EventDate, vm.EventDateSelection!.Value.LocalDateTime);
+ }
+
+ [Fact]
+ public void ApplyResolvedAddress_populates_empty_address_directly()
+ {
+ var api = new RecordingApi();
+ var client = new BillingApiClient(api, "https://business.example/api/v1/");
+ var vm =
+ new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" }
+ .CreateCommandPageViewModel(
+ new ActivityInfo { Code = "dev", Name = "Développement" },
+ new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" },
+ client) as RdvViewModel;
+
+ vm!.Address = string.Empty;
+ vm.ApplyResolvedAddress("10 rue de Rivoli, 75001 Paris");
+
+ Assert.Equal("10 rue de Rivoli, 75001 Paris", vm.Address);
+ Assert.False(vm.HasSuggestedAddress);
+ }
+
+ [Fact]
+ public void ApplyResolvedAddress_preserves_manual_address_and_exposes_suggestion()
+ {
+ var api = new RecordingApi();
+ var client = new BillingApiClient(api, "https://business.example/api/v1/");
+ var vm =
+ new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" }
+ .CreateCommandPageViewModel(
+ new ActivityInfo { Code = "dev", Name = "Développement" },
+ new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" },
+ client) as RdvViewModel;
+
+ vm!.Address = "Saisie manuelle";
+ vm.ApplyResolvedAddress("10 rue de Rivoli, 75001 Paris");
+
+ Assert.Equal("Saisie manuelle", vm.Address);
+ Assert.True(vm.HasSuggestedAddress);
+ Assert.Equal("10 rue de Rivoli, 75001 Paris", vm.SuggestedAddress);
+
+ vm.ApplySuggestedAddressCommand.Execute(null);
+
+ Assert.Equal("10 rue de Rivoli, 75001 Paris", vm.Address);
+ Assert.False(vm.HasSuggestedAddress);
+ }
+
[Fact]
public async Task InitializeAsync_loads_prestations_for_brush_and_submit_posts_selected_prestation()
{
diff --git a/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs b/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs
index a7f2b0844..d3d1d5694 100644
--- a/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs
+++ b/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs
@@ -45,10 +45,8 @@ public class BillingQueriesPageViewModelTests
Assert.Equal(2, vm.Queries.Count);
Assert.All(vm.Queries, q => Assert.DoesNotContain("Rejected", q.StatusLabel, StringComparison.OrdinalIgnoreCase));
Assert.Contains("lecture seule", vm.StatusMessage, StringComparison.OrdinalIgnoreCase);
- Assert.False(vm.CanOpenDetails);
- vm.SelectedQuery = vm.Queries[0];
- Assert.False(vm.OpenSelectedQueryCommand.CanExecute(null));
+ Assert.True(vm.Queries.Count > 0);
}
private sealed class StubBillingApi : IYavscApiClient
diff --git a/src/PostIt/PostIt.Tests/NominatimReverseGeocodingServiceTests.cs b/src/PostIt/PostIt.Tests/NominatimReverseGeocodingServiceTests.cs
new file mode 100644
index 000000000..4eceb7a2f
--- /dev/null
+++ b/src/PostIt/PostIt.Tests/NominatimReverseGeocodingServiceTests.cs
@@ -0,0 +1,72 @@
+using System.Net;
+using System.Text;
+using PostIt.Services;
+
+namespace PostIt.Tests;
+
+public class NominatimReverseGeocodingServiceTests
+{
+ [Fact]
+ public async Task TryResolveAddressAsync_formats_compact_street_address_from_nominatim_payload()
+ {
+ var handler = new StubHandler("""
+ {
+ "display_name": "6, Place de l'Hôtel-de-Ville - Esplanade de la Libération, Paris, 75004, France",
+ "address": {
+ "house_number": "6",
+ "road": "Place de l'Hôtel-de-Ville - Esplanade de la Libération",
+ "postcode": "75004",
+ "city": "Paris"
+ }
+ }
+ """);
+
+ var service = new NominatimReverseGeocodingService(new HttpClient(handler)
+ {
+ BaseAddress = new Uri("https://nominatim.openstreetmap.org/")
+ });
+
+ var result = await service.TryResolveAddressAsync(48.8566, 2.3522);
+
+ Assert.Equal("6, Place de l'Hôtel-de-Ville - Esplanade de la Libération, 75004, Paris", result);
+ Assert.NotNull(handler.LastRequest);
+ Assert.Contains("reverse?format=jsonv2", handler.LastRequest!.RequestUri!.ToString(), StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task TryResolveAddressAsync_returns_null_on_unsuccessful_response()
+ {
+ var handler = new StubHandler("{}", HttpStatusCode.TooManyRequests);
+ var service = new NominatimReverseGeocodingService(new HttpClient(handler)
+ {
+ BaseAddress = new Uri("https://nominatim.openstreetmap.org/")
+ });
+
+ var result = await service.TryResolveAddressAsync(48.8566, 2.3522);
+
+ Assert.Null(result);
+ }
+
+ private sealed class StubHandler : HttpMessageHandler
+ {
+ private readonly string _payload;
+ private readonly HttpStatusCode _statusCode;
+
+ public HttpRequestMessage? LastRequest { get; private set; }
+
+ public StubHandler(string payload, HttpStatusCode statusCode = HttpStatusCode.OK)
+ {
+ _payload = payload;
+ _statusCode = statusCode;
+ }
+
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ LastRequest = request;
+ return Task.FromResult(new HttpResponseMessage(_statusCode)
+ {
+ Content = new StringContent(_payload, Encoding.UTF8, "application/json")
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PostIt/PostIt.Tests/RdvPageHeadlessTests.cs b/src/PostIt/PostIt.Tests/RdvPageHeadlessTests.cs
new file mode 100644
index 000000000..fdf8689ab
--- /dev/null
+++ b/src/PostIt/PostIt.Tests/RdvPageHeadlessTests.cs
@@ -0,0 +1,79 @@
+using Avalonia.Controls;
+using Avalonia.Headless.XUnit;
+using PostIt.ViewModels;
+using PostIt.ViewModels.Commands;
+using PostIt.Views.Commands;
+using Yavsc.Abstract.Workflow;
+using Yavsc.Api.Client;
+
+namespace PostIt.Tests;
+
+public class RdvPageHeadlessTests
+{
+ [AvaloniaFact]
+ public void Suggested_address_panel_is_hidden_by_default()
+ {
+ var page = CreatePage(out _);
+
+ var panel = page.FindControl("SuggestedAddressPanel");
+ var progress = page.FindControl("SuggestedAddressProgress");
+
+ Assert.NotNull(panel);
+ Assert.NotNull(progress);
+ Assert.False(panel!.IsVisible);
+ Assert.False(progress!.IsVisible);
+ }
+
+ [AvaloniaFact]
+ public async Task Suggested_address_panel_and_spinner_follow_viewmodel_state()
+ {
+ var page = CreatePage(out var vm);
+ var panel = page.FindControl("SuggestedAddressPanel")!;
+ var progress = page.FindControl("SuggestedAddressProgress")!;
+ var applyButton = page.FindControl
diff --git a/src/PostIt/PostIt/Views/MainPage.axaml.cs b/src/PostIt/PostIt/Views/Blogs/MainPage.axaml.cs
similarity index 100%
rename from src/PostIt/PostIt/Views/MainPage.axaml.cs
rename to src/PostIt/PostIt/Views/Blogs/MainPage.axaml.cs
diff --git a/src/PostIt/PostIt/Views/Commands/BrushPage.axaml b/src/PostIt/PostIt/Views/Commands/BrushPage.axaml
index 9c33cecb2..d01ea4a1b 100644
--- a/src/PostIt/PostIt/Views/Commands/BrushPage.axaml
+++ b/src/PostIt/PostIt/Views/Commands/BrushPage.axaml
@@ -3,6 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cvm="using:PostIt.ViewModels.Commands"
+ xmlns:postitControls="using:PostIt.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="PostIt.Views.Commands.BrushPage"
@@ -93,16 +94,8 @@
Content="{Binding SubmitLabel}"
Command="{Binding SubmitCommand}"
IsEnabled="{Binding IsSupported}" />
-
+
-
+
-
+
-
+
+
+
+
+
-
-
+
+
+
+
-
-
+
+
+
+
-
+
+
+
-
-
-
+
-
+
("LocationMap");
+ if (_locationMap is null)
+ return;
+
+ var map = new Map();
+ map.Layers.Add(OpenStreetMap.CreateTileLayer());
+ _selectionLayer = CreateSelectionLayer();
+ map.Layers.Add(_selectionLayer);
+ _locationMap.Map = map;
+ _locationMap.MapTapped += OnMapTapped;
+
+ DataContextChanged += OnDataContextChanged;
+ AttachViewModel(DataContext as RdvViewModel);
+ }
+
+ private void OnDataContextChanged(object? sender, System.EventArgs e)
+ {
+ AttachViewModel(DataContext as RdvViewModel);
+ CenterFromViewModel();
+ }
+
+ private async void OnMapTapped(object? sender, MapEventArgs e)
+ {
+ if (DataContext is not RdvViewModel vm)
+ return;
+
+ var (longitude, latitude) = SphericalMercator.ToLonLat(e.WorldPosition.X, e.WorldPosition.Y);
+ vm.ApplyLocationFromMap(latitude, longitude);
+ UpdateMarkerFromViewModel();
+ CenterMap(latitude, longitude, zoomLevel: SelectedZoomLevel);
+ await TryResolveAddressAsync(vm, latitude, longitude);
+ }
+
+ private async void OnCenterCurrentLocationClicked(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ if (DataContext is not RdvViewModel vm || !vm.CanUseCurrentLocation)
+ return;
+
+ await vm.UseCurrentLocationCommand.ExecuteAsync(null);
+ UpdateMarkerFromViewModel();
+ CenterFromViewModel();
+
+ if (vm.Latitude.HasValue && vm.Longitude.HasValue)
+ await TryResolveAddressAsync(vm, vm.Latitude.Value, vm.Longitude.Value);
+ }
+
+ private void AttachViewModel(RdvViewModel? vm)
+ {
+ if (_currentViewModel is not null)
+ _currentViewModel.PropertyChanged -= OnViewModelPropertyChanged;
+
+ _currentViewModel = vm;
+
+ if (_currentViewModel is not null)
+ _currentViewModel.PropertyChanged += OnViewModelPropertyChanged;
+
+ UpdateMarkerFromViewModel();
+ }
+
+ private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(RdvViewModel.Latitude)
+ || e.PropertyName == nameof(RdvViewModel.Longitude))
+ {
+ UpdateMarkerFromViewModel();
+ }
+ }
+
+ private void CenterFromViewModel()
+ {
+ if (DataContext is not RdvViewModel vm)
+ {
+ CenterMap(DefaultLatitude, DefaultLongitude, zoomLevel: DefaultZoomLevel);
+ return;
+ }
+
+ if (vm.Latitude.HasValue && vm.Longitude.HasValue)
+ {
+ CenterMap(vm.Latitude.Value, vm.Longitude.Value, zoomLevel: SelectedZoomLevel);
+ return;
+ }
+
+ CenterMap(DefaultLatitude, DefaultLongitude, zoomLevel: DefaultZoomLevel);
+ }
+
+ private void CenterMap(double latitude, double longitude, int zoomLevel)
+ {
+ if (_locationMap?.Map is null)
+ return;
+
+ var (x, y) = SphericalMercator.FromLonLat(longitude, latitude);
+ _locationMap.Map.Navigator.CenterOn(x, y);
+ _locationMap.Map.Navigator.ZoomToLevel(zoomLevel);
+ }
+
+ private void UpdateMarkerFromViewModel()
+ {
+ if (_selectionLayer is null)
+ return;
+
+ if (DataContext is not RdvViewModel vm
+ || !vm.Latitude.HasValue
+ || !vm.Longitude.HasValue)
+ {
+ _selectionLayer.Features = Enumerable.Empty();
+ _locationMap?.RefreshData(ChangeType.Discrete);
+ return;
+ }
+
+ var (x, y) = SphericalMercator.FromLonLat(vm.Longitude.Value, vm.Latitude.Value);
+ _selectionLayer.Features = new IFeature[] { new PointFeature(x, y) };
+ _locationMap?.RefreshData(ChangeType.Discrete);
+ }
+
+ private static MemoryLayer CreateSelectionLayer()
+ {
+ return new MemoryLayer("selected-location")
+ {
+ Style = new SymbolStyle
+ {
+ SymbolType = SymbolType.Ellipse,
+ Fill = new Brush(Color.Crimson),
+ Outline = new Pen(Color.White, 2),
+ SymbolScale = 0.9,
+ },
+ Features = Enumerable.Empty(),
+ };
+ }
+
+ private async Task TryResolveAddressAsync(RdvViewModel vm, double latitude, double longitude)
+ {
+ if (_lastResolvedLatitude.HasValue
+ && _lastResolvedLongitude.HasValue
+ && AreCoordinatesClose(_lastResolvedLatitude.Value, _lastResolvedLongitude.Value, latitude, longitude)
+ && !string.IsNullOrWhiteSpace(_lastResolvedAddress))
+ {
+ vm.ApplyResolvedAddress(_lastResolvedAddress);
+ return;
+ }
+
+ _reverseGeocodeCts?.Cancel();
+ _reverseGeocodeCts?.Dispose();
+ _reverseGeocodeCts = new CancellationTokenSource();
+ var cancellationToken = _reverseGeocodeCts.Token;
+
+ try
+ {
+ vm.NotifyReverseGeocodingStarted();
+ await Task.Delay(ReverseGeocodingDebounce, cancellationToken).ConfigureAwait(true);
+
+ var resolved = await _reverseGeocodingService
+ .TryResolveAddressAsync(latitude, longitude, cancellationToken)
+ .ConfigureAwait(true);
+
+ if (!string.IsNullOrWhiteSpace(resolved))
+ {
+ _lastResolvedLatitude = latitude;
+ _lastResolvedLongitude = longitude;
+ _lastResolvedAddress = resolved;
+ vm.ApplyResolvedAddress(resolved);
+ return;
+ }
+
+ vm.NotifyReverseGeocodingUnavailable();
+ }
+ catch (OperationCanceledException)
+ {
+ vm.IsResolvingAddress = false;
+ }
+ }
+
+ internal static bool AreCoordinatesClose(
+ double latitudeA,
+ double longitudeA,
+ double latitudeB,
+ double longitudeB)
+ {
+ return Math.Abs(latitudeA - latitudeB) <= ReverseGeocodingCacheToleranceDegrees
+ && Math.Abs(longitudeA - longitudeB) <= ReverseGeocodingCacheToleranceDegrees;
+ }
}
diff --git a/src/PostIt/PostIt/Views/HomePage.axaml b/src/PostIt/PostIt/Views/Layout/HomePage.axaml
similarity index 100%
rename from src/PostIt/PostIt/Views/HomePage.axaml
rename to src/PostIt/PostIt/Views/Layout/HomePage.axaml
diff --git a/src/PostIt/PostIt/Views/HomePage.axaml.cs b/src/PostIt/PostIt/Views/Layout/HomePage.axaml.cs
similarity index 100%
rename from src/PostIt/PostIt/Views/HomePage.axaml.cs
rename to src/PostIt/PostIt/Views/Layout/HomePage.axaml.cs
diff --git a/src/PostIt/PostIt/Views/MainView.axaml b/src/PostIt/PostIt/Views/Layout/MainView.axaml
similarity index 100%
rename from src/PostIt/PostIt/Views/MainView.axaml
rename to src/PostIt/PostIt/Views/Layout/MainView.axaml
diff --git a/src/PostIt/PostIt/Views/MainView.axaml.cs b/src/PostIt/PostIt/Views/Layout/MainView.axaml.cs
similarity index 100%
rename from src/PostIt/PostIt/Views/MainView.axaml.cs
rename to src/PostIt/PostIt/Views/Layout/MainView.axaml.cs
diff --git a/src/PostIt/PostIt/Views/MainWindow.axaml b/src/PostIt/PostIt/Views/Layout/MainWindow.axaml
similarity index 100%
rename from src/PostIt/PostIt/Views/MainWindow.axaml
rename to src/PostIt/PostIt/Views/Layout/MainWindow.axaml
diff --git a/src/PostIt/PostIt/Views/MainWindow.axaml.cs b/src/PostIt/PostIt/Views/Layout/MainWindow.axaml.cs
similarity index 100%
rename from src/PostIt/PostIt/Views/MainWindow.axaml.cs
rename to src/PostIt/PostIt/Views/Layout/MainWindow.axaml.cs
diff --git a/src/PostIt/PostIt/Views/SessionStatusBanner.axaml b/src/PostIt/PostIt/Views/Layout/SessionStatusBanner.axaml
similarity index 100%
rename from src/PostIt/PostIt/Views/SessionStatusBanner.axaml
rename to src/PostIt/PostIt/Views/Layout/SessionStatusBanner.axaml
diff --git a/src/PostIt/PostIt/Views/SessionStatusBanner.axaml.cs b/src/PostIt/PostIt/Views/Layout/SessionStatusBanner.axaml.cs
similarity index 100%
rename from src/PostIt/PostIt/Views/SessionStatusBanner.axaml.cs
rename to src/PostIt/PostIt/Views/Layout/SessionStatusBanner.axaml.cs
diff --git a/src/PostIt/PostIt/Views/SettingsPage.axaml b/src/PostIt/PostIt/Views/Layout/SettingsPage.axaml
similarity index 95%
rename from src/PostIt/PostIt/Views/SettingsPage.axaml
rename to src/PostIt/PostIt/Views/Layout/SettingsPage.axaml
index bb19dfa3c..2973679b5 100644
--- a/src/PostIt/PostIt/Views/SettingsPage.axaml
+++ b/src/PostIt/PostIt/Views/Layout/SettingsPage.axaml
@@ -2,6 +2,7 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="cl:avalonia.Controls"
+ xmlns:postitControls="using:PostIt.Controls"
x:Class="PostIt.Views.SettingsPage"
xmlns:vm="using:PostIt.ViewModels"
x:DataType="vm:Settings"
@@ -51,9 +52,7 @@
Opacity="0.8"
TextWrapping="Wrap" />
-
+
();
var message = await api.SetAvatarAsync(stream, file.Name, GetMimeType(file.Name));
SetAvatarStatus(string.IsNullOrWhiteSpace(message)
- ? "Avatar mis à jour."
- : message, Brushes.ForestGreen);
+ ? "Avatar mis a jour."
+ : message,
+ StatusSeverity.Info);
_ = ClearSuccessStatusLaterAsync(statusVersion);
}
catch (Exception ex)
{
- SetAvatarStatus($"Échec upload avatar: {ex.Message}", Brushes.IndianRed);
+ SetAvatarStatus($"Echec upload avatar: {ex.Message}", StatusSeverity.Error);
Console.Error.WriteLine($"🩎 Avatar upload failed: {ex.Message}");
}
finally
@@ -89,16 +90,15 @@ public partial class SettingsPage: ContentPage
if (statusVersion != _avatarStatusVersion)
return;
- if (AvatarUploadStatusText.Foreground == Brushes.ForestGreen)
- {
- SetAvatarStatus($"Avatar prêt. Formats supportés: {AcceptedAvatarFormats}. Taille max: {MaxAvatarSizeMegabytes} MB.", Brushes.Gray);
- }
+ SetAvatarStatus($"Avatar pret. Formats supportes: {AcceptedAvatarFormats}. Taille max: {MaxAvatarSizeMegabytes} MB.", StatusSeverity.Info);
}
- private void SetAvatarStatus(string message, IBrush color)
+ private void SetAvatarStatus(string message, StatusSeverity severity)
{
- AvatarUploadStatusText.Foreground = color;
- AvatarUploadStatusText.Text = message;
+ if (DataContext is Settings settings)
+ {
+ settings.SetActionStatus(message, severity);
+ }
}
private static string GetMimeType(string fileName)
diff --git a/src/PostIt/PostIt/Views/SignaturePage.axaml b/src/PostIt/PostIt/Views/Signature/SignaturePage.axaml
similarity index 94%
rename from src/PostIt/PostIt/Views/SignaturePage.axaml
rename to src/PostIt/PostIt/Views/Signature/SignaturePage.axaml
index d868f2a48..021208b54 100644
--- a/src/PostIt/PostIt/Views/SignaturePage.axaml
+++ b/src/PostIt/PostIt/Views/Signature/SignaturePage.axaml
@@ -3,6 +3,7 @@
xmlns:d="http://github.com/avaloniaui/avalonia"
xmlns:vm="using:PostIt.ViewModels"
xmlns:controls="using:PostIt.Controls"
+ xmlns:postitControls="using:PostIt.Controls"
x:Class="PostIt.Views.SignaturePage"
x:DataType="vm:SignaturePageViewModel"
HorizontalAlignment="Stretch"
@@ -59,10 +60,8 @@
-
+
diff --git a/src/Yavsc.Abstract/Blogspot/BlogPostDto.cs b/src/Yavsc.Abstract/Blogspot/BlogPostDto.cs
index 9cb5b1430..bdb2ccdc8 100644
--- a/src/Yavsc.Abstract/Blogspot/BlogPostDto.cs
+++ b/src/Yavsc.Abstract/Blogspot/BlogPostDto.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using Yavsc.Abstract.Identity.Security;
using System.Text.Json.Serialization;
diff --git a/src/Yavsc.Abstract/Blogspot/IBlogPost.cs b/src/Yavsc.Abstract/Blogspot/IBlogPost.cs
index 1ef371803..38a50b78f 100644
--- a/src/Yavsc.Abstract/Blogspot/IBlogPost.cs
+++ b/src/Yavsc.Abstract/Blogspot/IBlogPost.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
diff --git a/src/Yavsc.Abstract/Chat/ChatHubConstants.cs b/src/Yavsc.Abstract/Chat/ChatHubConstants.cs
index b54c00c74..c673e6fae 100644
--- a/src/Yavsc.Abstract/Chat/ChatHubConstants.cs
+++ b/src/Yavsc.Abstract/Chat/ChatHubConstants.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
namespace Yavsc.Abstract.Chat
{
public static class ChatHubConstants
diff --git a/src/Yavsc.Abstract/Chat/IChatRoom.cs b/src/Yavsc.Abstract/Chat/IChatRoom.cs
index 1d6f68dd5..133ca2837 100644
--- a/src/Yavsc.Abstract/Chat/IChatRoom.cs
+++ b/src/Yavsc.Abstract/Chat/IChatRoom.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
namespace Yavsc.Abstract.Chat
diff --git a/src/Yavsc.Abstract/FileSystem/AbstractFileSystemHelpers.cs b/src/Yavsc.Abstract/FileSystem/AbstractFileSystemHelpers.cs
index dd805864e..14bc86435 100644
--- a/src/Yavsc.Abstract/FileSystem/AbstractFileSystemHelpers.cs
+++ b/src/Yavsc.Abstract/FileSystem/AbstractFileSystemHelpers.cs
@@ -1,4 +1,6 @@
-using System.Text;
+#nullable enable annotations
+
+using System.Text;
using Yavsc.ViewModels.UserFiles;
namespace Yavsc.Server.Helpers
diff --git a/src/Yavsc.Abstract/FileSystem/FsOperationInfo.cs b/src/Yavsc.Abstract/FileSystem/FsOperationInfo.cs
index 76a481492..0e3c0e0c7 100644
--- a/src/Yavsc.Abstract/FileSystem/FsOperationInfo.cs
+++ b/src/Yavsc.Abstract/FileSystem/FsOperationInfo.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
namespace Yavsc.Abstract.Helpers
{
public enum ErrorCode {
diff --git a/src/Yavsc.Abstract/FileSystem/UserDirectoryInfo.cs b/src/Yavsc.Abstract/FileSystem/UserDirectoryInfo.cs
index fa727d0c0..2ec5dcfaf 100644
--- a/src/Yavsc.Abstract/FileSystem/UserDirectoryInfo.cs
+++ b/src/Yavsc.Abstract/FileSystem/UserDirectoryInfo.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using Yavsc.Server.Helpers;
namespace Yavsc.ViewModels.UserFiles
diff --git a/src/Yavsc.Abstract/Google/GDate.cs b/src/Yavsc.Abstract/Google/GDate.cs
index 3506ba416..10e4de92e 100644
--- a/src/Yavsc.Abstract/Google/GDate.cs
+++ b/src/Yavsc.Abstract/Google/GDate.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
//
// GDate.cs
//
diff --git a/src/Yavsc.Abstract/Google/Messaging/MessageWithPayloadResponse.cs b/src/Yavsc.Abstract/Google/Messaging/MessageWithPayloadResponse.cs
index 59538ca62..980a4abcf 100644
--- a/src/Yavsc.Abstract/Google/Messaging/MessageWithPayloadResponse.cs
+++ b/src/Yavsc.Abstract/Google/Messaging/MessageWithPayloadResponse.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
//
// MessageWithPayloadResponse.cs
//
diff --git a/src/Yavsc.Abstract/IT/Fixing/Bug.cs b/src/Yavsc.Abstract/IT/Fixing/Bug.cs
index f0b403bb7..60bd155f6 100644
--- a/src/Yavsc.Abstract/IT/Fixing/Bug.cs
+++ b/src/Yavsc.Abstract/IT/Fixing/Bug.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Yavsc.Attributes.Validation;
diff --git a/src/Yavsc.Abstract/Identity/IApplicationUser.cs b/src/Yavsc.Abstract/Identity/IApplicationUser.cs
index 5d1d5b1ca..0b8e1047c 100644
--- a/src/Yavsc.Abstract/Identity/IApplicationUser.cs
+++ b/src/Yavsc.Abstract/Identity/IApplicationUser.cs
@@ -1,4 +1,6 @@
-namespace Yavsc.Abstract.Identity
+#nullable enable annotations
+
+namespace Yavsc.Abstract.Identity
{
public interface IApplicationUser
{
diff --git a/src/Yavsc.Abstract/Identity/Security/FileAccessControlRulePayload.cs b/src/Yavsc.Abstract/Identity/Security/FileAccessControlRulePayload.cs
new file mode 100644
index 000000000..af3af22bc
--- /dev/null
+++ b/src/Yavsc.Abstract/Identity/Security/FileAccessControlRulePayload.cs
@@ -0,0 +1,12 @@
+
+namespace Yavsc.Models.Access
+{
+ using Yavsc.Abstract.Identity.Security;
+
+ public class FileAccessControlRulePayload : CircleAuthorization
+ {
+ public virtual string Path { get; set; }
+
+
+ }
+}
diff --git a/src/Yavsc.Abstract/Identity/UserDisplayHelpers.cs b/src/Yavsc.Abstract/Identity/UserDisplayHelpers.cs
index 242615527..4277d6e3b 100644
--- a/src/Yavsc.Abstract/Identity/UserDisplayHelpers.cs
+++ b/src/Yavsc.Abstract/Identity/UserDisplayHelpers.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
namespace Yavsc.Abstract.Identity
{
///
diff --git a/src/Yavsc.Abstract/Interfaces/Workflow/IBookQueryData.cs b/src/Yavsc.Abstract/Interfaces/Workflow/IBookQueryData.cs
index 2c844f925..eb3dee591 100644
--- a/src/Yavsc.Abstract/Interfaces/Workflow/IBookQueryData.cs
+++ b/src/Yavsc.Abstract/Interfaces/Workflow/IBookQueryData.cs
@@ -1,4 +1,6 @@
-using Yavsc.Abstract.Identity;
+#nullable enable annotations
+
+using Yavsc.Abstract.Identity;
namespace Yavsc.Interfaces
{
diff --git a/src/Yavsc.Abstract/Messaging/Comment.cs b/src/Yavsc.Abstract/Messaging/Comment.cs
index 3b8702ae1..d8eef456c 100644
--- a/src/Yavsc.Abstract/Messaging/Comment.cs
+++ b/src/Yavsc.Abstract/Messaging/Comment.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using Yavsc.Interfaces;
diff --git a/src/Yavsc.Abstract/Messaging/Notification.cs b/src/Yavsc.Abstract/Messaging/Notification.cs
index dbbf735d5..f7b35b48b 100644
--- a/src/Yavsc.Abstract/Messaging/Notification.cs
+++ b/src/Yavsc.Abstract/Messaging/Notification.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
diff --git a/src/Yavsc.Abstract/Messaging/RdvQueryEvent.cs b/src/Yavsc.Abstract/Messaging/RdvQueryEvent.cs
index b2168bd7c..9e2ecb754 100644
--- a/src/Yavsc.Abstract/Messaging/RdvQueryEvent.cs
+++ b/src/Yavsc.Abstract/Messaging/RdvQueryEvent.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
//
// BookQueryEvent.cs
//
diff --git a/src/Yavsc.Abstract/Messaging/RdvQueryProviderInfo.cs b/src/Yavsc.Abstract/Messaging/RdvQueryProviderInfo.cs
index 87125c544..94eeb5ab9 100644
--- a/src/Yavsc.Abstract/Messaging/RdvQueryProviderInfo.cs
+++ b/src/Yavsc.Abstract/Messaging/RdvQueryProviderInfo.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using Yavsc.Abstract.Identity;
using Yavsc.Models.Relationship;
diff --git a/src/Yavsc.Abstract/Workflow/IMobileDeviceDeclaration.cs b/src/Yavsc.Abstract/Workflow/IMobileDeviceDeclaration.cs
index 6e55ebe51..7a1dbc261 100644
--- a/src/Yavsc.Abstract/Workflow/IMobileDeviceDeclaration.cs
+++ b/src/Yavsc.Abstract/Workflow/IMobileDeviceDeclaration.cs
@@ -1,4 +1,6 @@
-// Copyright (C) 2016 Paul Schneider
+#nullable enable annotations
+
+// Copyright (C) 2016 Paul Schneider
//
// This file is part of yavsc.
//
diff --git a/src/Yavsc.Abstract/Workflow/IPerformerProfile.cs b/src/Yavsc.Abstract/Workflow/IPerformerProfile.cs
index 4314dd371..606285a6c 100644
--- a/src/Yavsc.Abstract/Workflow/IPerformerProfile.cs
+++ b/src/Yavsc.Abstract/Workflow/IPerformerProfile.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
namespace Yavsc.Workflow
{
public interface IPerformerProfile
diff --git a/src/Yavsc.Abstract/Workflow/PerformerCodeInputValidation.cs b/src/Yavsc.Abstract/Workflow/PerformerCodeInputValidation.cs
index 84e9c1cc7..4ed60e465 100644
--- a/src/Yavsc.Abstract/Workflow/PerformerCodeInputValidation.cs
+++ b/src/Yavsc.Abstract/Workflow/PerformerCodeInputValidation.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
diff --git a/src/Yavsc.Api.Test/Fixtures/ApiWebServerFixture.cs b/src/Yavsc.Api.Test/Fixtures/ApiWebServerFixture.cs
index ca4ae70f9..e2a8b3376 100644
--- a/src/Yavsc.Api.Test/Fixtures/ApiWebServerFixture.cs
+++ b/src/Yavsc.Api.Test/Fixtures/ApiWebServerFixture.cs
@@ -204,6 +204,10 @@ public sealed class ApiWebServerFixture : WebHostFixture
ClientId = "alice",
PerformerId = "alice",
Consent = true,
+ UserCreated = "alice",
+ UserModified = "alice",
+ DateCreated = DateTime.UtcNow,
+ DateModified = DateTime.UtcNow,
EventDate = DateTime.UtcNow.AddDays(1),
Location = location,
Reason = "Initial rendez-vous",
@@ -293,6 +297,10 @@ public sealed class ApiWebServerFixture : WebHostFixture
ClientId = "alice",
PerformerId = "alice",
Consent = true,
+ UserCreated = "alice",
+ UserModified = "alice",
+ DateCreated = DateTime.UtcNow,
+ DateModified = DateTime.UtcNow,
EventDate = DateTime.UtcNow.AddDays(3),
Location = location,
PrestationId = prestation1.Id,
@@ -308,6 +316,10 @@ public sealed class ApiWebServerFixture : WebHostFixture
ClientId = "alice",
PerformerId = "alice",
Consent = true,
+ UserCreated = "alice",
+ UserModified = "alice",
+ DateCreated = DateTime.UtcNow,
+ DateModified = DateTime.UtcNow,
EventDate = DateTime.UtcNow.AddDays(4),
Location = location,
Prestations = new List
diff --git a/src/Yavsc.Api.Test/FrontOfficeApiControllerTests.cs b/src/Yavsc.Api.Test/FrontOfficeApiControllerTests.cs
new file mode 100644
index 000000000..97ed2e61c
--- /dev/null
+++ b/src/Yavsc.Api.Test/FrontOfficeApiControllerTests.cs
@@ -0,0 +1,62 @@
+using System.Net;
+using System.Net.Http.Headers;
+using Microsoft.Extensions.DependencyInjection;
+using Yavsc.Api.Test.Fixtures;
+using Yavsc.Helpers;
+using Yavsc.Models;
+using Yavsc.Tests.Shared;
+
+namespace Yavsc.Api.Test;
+
+[Collection("Yavsc Api")]
+public sealed class FrontOfficeApiControllerTests : IClassFixture
+{
+ private readonly ApiWebServerFixture _fixture;
+
+ public FrontOfficeApiControllerTests(ApiWebServerFixture fixture)
+ {
+ _fixture = fixture;
+ }
+
+ private HttpClient NewClient(string subject = "alice", string scope = "api")
+ {
+ var handler = new HttpClientHandler
+ {
+ ServerCertificateCustomValidationCallback = (_, _, _, _) => true
+ };
+
+ var http = new HttpClient(handler)
+ {
+ BaseAddress = new Uri(_fixture.BaseAddress)
+ };
+ http.DefaultRequestHeaders.Authorization =
+ new AuthenticationHeaderValue("Bearer", TestTokenIssuer.Issue(subject, scope));
+ return http;
+ }
+
+ [Fact]
+ public async Task Front_accept_query_updates_status_without_server_error()
+ {
+ WorkflowHelpers.ConfigureBillingService();
+ _fixture.ResetAndSeedRdvQueryGraph();
+
+ long queryId;
+ using (var scope = _fixture.Services.CreateScope())
+ {
+ var db = scope.ServiceProvider.GetRequiredService();
+ queryId = db.RdvQueries.Select(q => q.Id).Single();
+ }
+
+ using var http = NewClient();
+ var response = await http.PostAsync($"/api/v1/front/query/accept?billingCode=Rdv&queryId={queryId}", content: null, TestContext.Current.CancellationToken);
+ var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
+
+ Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}");
+
+ using var assertScope = _fixture.Services.CreateScope();
+ var assertDb = assertScope.ServiceProvider.GetRequiredService();
+ var updated = assertDb.RdvQueries.Single(q => q.Id == queryId);
+
+ Assert.Equal(QueryStatus.Accepted, updated.Status);
+ }
+}
diff --git a/src/Yavsc.Api.Test/RdvQueryApiControllerTests.cs b/src/Yavsc.Api.Test/RdvQueryApiControllerTests.cs
index 8fe7e959e..668814239 100644
--- a/src/Yavsc.Api.Test/RdvQueryApiControllerTests.cs
+++ b/src/Yavsc.Api.Test/RdvQueryApiControllerTests.cs
@@ -82,4 +82,65 @@ public sealed class RdvQueryApiControllerTests : IClassFixture(TestContext.Current.CancellationToken);
+ Assert.NotNull(created);
+ Assert.Equal("alice", created!.ClientId);
+ }
+
+ [Fact]
+ public async Task PostQuery_accepts_local_datetime_and_persists_as_utc()
+ {
+ _fixture.ResetAndSeedRdvQueryGraph();
+ using var http = NewClient(subject: "alice");
+
+ var localEventDate = DateTime.Now.AddDays(2);
+ var createPayload = new
+ {
+ ActivityCode = "dev",
+ PerformerId = "alice",
+ Consent = true,
+ EventDate = localEventDate,
+ Location = new
+ {
+ Address = "3 rue du Test",
+ Latitude = 48.8568,
+ Longitude = 2.3524,
+ },
+ Reason = "Rendez-vous date locale",
+ Status = QueryStatus.Inserted,
+ };
+
+ var createResponse = await http.PostAsJsonAsync("/api/v1/billing/Rdv", createPayload, TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
+
+ var created = await createResponse.Content.ReadFromJsonAsync(TestContext.Current.CancellationToken);
+ Assert.NotNull(created);
+ Assert.Equal(DateTimeKind.Utc, created!.EventDate.Kind);
+ }
}
diff --git a/src/Yavsc.Api/Controllers/Business/ActivityApiController.cs b/src/Yavsc.Api/Controllers/Business/ActivityApiController.cs
index a8d288063..aef393868 100644
--- a/src/Yavsc.Api/Controllers/Business/ActivityApiController.cs
+++ b/src/Yavsc.Api/Controllers/Business/ActivityApiController.cs
@@ -51,6 +51,14 @@ namespace Yavsc.Controllers
.Distinct()
.ToArray();
+ // Some providers are brittle when translating Contains over an
+ // empty in-memory array. If there is no candidate activity code,
+ // the catalog is empty by definition.
+ if (codes.Length == 0)
+ {
+ return Ok(new List());
+ }
+
var performerCounts = await (
from ua in _context.UserActivities.AsNoTracking()
where !string.IsNullOrWhiteSpace(ua.DoesCode) && codes.Contains(ua.DoesCode)
@@ -64,10 +72,10 @@ namespace Yavsc.Controllers
var filteredActivities = activities
.Where(a =>
- (performerCounts.TryGetValue(a.Code, out var ownCount) && ownCount > 0)
+ (TryGetPerformerCount(performerCounts, a.Code, out var ownCount) && ownCount > 0)
|| (a.Children ?? new List())
.Where(c => !c.Hidden)
- .Any(c => performerCounts.TryGetValue(c.Code, out var childCount) && childCount > 0))
+ .Any(c => TryGetPerformerCount(performerCounts, c.Code, out var childCount) && childCount > 0))
.ToList();
return Ok(filteredActivities.Select(a => ToBrowseItem(a, performerCounts)).ToList());
@@ -269,10 +277,10 @@ namespace Yavsc.Controllers
Code = activity.Code,
Name = activity.Name,
ParentCode = activity.ParentCode,
- Description = activity.Description,
+ Description = activity.Description ?? string.Empty,
Photo = activity.Photo,
Rate = activity.Rate,
- PerformerCount = performerCounts.TryGetValue(activity.Code, out var count) ? count : 0,
+ PerformerCount = TryGetPerformerCount(performerCounts, activity.Code, out var count) ? count : 0,
Forms = (activity.Forms ?? Enumerable.Empty())
.Select(f => new CommandFormSummary
{
@@ -283,17 +291,17 @@ namespace Yavsc.Controllers
.ToList(),
Children = (activity.Children ?? Enumerable.Empty())
.Where(c => !c.Hidden)
- .Where(c => performerCounts.TryGetValue(c.Code, out var childCount) && childCount > 0)
+ .Where(c => TryGetPerformerCount(performerCounts, c.Code, out var childCount) && childCount > 0)
.OrderByDescending(c => c.Rate)
.Select(c => new ActivityInfo
{
Code = c.Code,
Name = c.Name,
ParentCode = c.ParentCode,
- Description = c.Description,
+ Description = c.Description ?? string.Empty,
Photo = c.Photo,
Rate = c.Rate,
- PerformerCount = performerCounts.TryGetValue(c.Code, out var childCount) ? childCount : 0,
+ PerformerCount = TryGetPerformerCount(performerCounts, c.Code, out var childCount) ? childCount : 0,
Forms = (c.Forms ?? Enumerable.Empty())
.Select(f => new CommandFormSummary
{
@@ -306,5 +314,19 @@ namespace Yavsc.Controllers
.ToList(),
};
}
+
+ private static bool TryGetPerformerCount(
+ IReadOnlyDictionary performerCounts,
+ string code,
+ out int count)
+ {
+ if (string.IsNullOrWhiteSpace(code))
+ {
+ count = 0;
+ return false;
+ }
+
+ return performerCounts.TryGetValue(code, out count);
+ }
}
}
diff --git a/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs b/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs
index a4ecbf2f8..3adac5b03 100644
--- a/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs
+++ b/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
using Yavsc.Helpers;
using Yavsc.Models;
using Yavsc.Services;
+using Yavsc.Server.Helpers;
using Yavsc.ViewModels.FrontOffice;
namespace Yavsc.ApiControllers
@@ -15,10 +16,10 @@ namespace Yavsc.ApiControllers
private IBillingService billing;
- public FrontOfficeApiController(ApplicationDbContext context, IBillingService billing)
+ public FrontOfficeApiController(ApplicationDbContext context, IBillingService billing = null)
{
dbContext = context;
- this.billing = billing;
+ this.billing = billing ?? new BillingService(context);
}
[HttpGet("profiles/{actCode}")]
@@ -36,7 +37,7 @@ namespace Yavsc.ApiControllers
if (billing == null) return BadRequest();
billing.Status = QueryStatus.Rejected;
- dbContext.SaveChanges();
+ dbContext.SaveChanges(User.GetUserId());
return Ok();
}
@@ -48,7 +49,7 @@ namespace Yavsc.ApiControllers
var billing = BillingService.GetBillable(dbContext, billingCode, queryId);
if (billing == null) return BadRequest();
billing.Status = QueryStatus.Accepted;
- dbContext.SaveChanges();
+ dbContext.SaveChanges(User.GetUserId());
return Ok();
}
}
diff --git a/src/Yavsc.Api/Controllers/Business/HairCutQueryApiController.cs b/src/Yavsc.Api/Controllers/Business/HairCutQueryApiController.cs
index 6c7c1ba68..7f445fdd9 100644
--- a/src/Yavsc.Api/Controllers/Business/HairCutQueryApiController.cs
+++ b/src/Yavsc.Api/Controllers/Business/HairCutQueryApiController.cs
@@ -82,19 +82,17 @@ public class HairCutQueryApiController : Controller
public async Task PostQuery([FromBody] HairCutQuery query, CancellationToken cancellationToken)
{
var uid = User.GetUserId();
- if (string.IsNullOrWhiteSpace(query.ClientId))
- {
- query.ClientId = uid;
- }
+ query.ClientId = uid;
+ ModelState.Remove("Client");
ModelState.Remove("ClientId");
+ ModelState.Remove("UserCreated");
+ ModelState.Remove("UserModified");
+ ModelState.Remove("SelectedProfile");
ModelState.Remove("Prestation");
-
- if (query.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
- {
- ModelState.AddModelError("ClientId", "You can only create your own HairCutQuery");
- return BadRequest(ModelState);
- }
+ ModelState.Remove("PerformerProfile");
+ ModelState.Remove("Context");
+ ModelState.Remove("Regularization");
query.Prestation = await _context.HairPrestation
.SingleOrDefaultAsync(p => p.Id == query.PrestationId, cancellationToken);
diff --git a/src/Yavsc.Api/Controllers/Business/HairMultiCutQueryApiController.cs b/src/Yavsc.Api/Controllers/Business/HairMultiCutQueryApiController.cs
index f850b3f41..3b33c4160 100644
--- a/src/Yavsc.Api/Controllers/Business/HairMultiCutQueryApiController.cs
+++ b/src/Yavsc.Api/Controllers/Business/HairMultiCutQueryApiController.cs
@@ -89,18 +89,16 @@ public class HairMultiCutQueryApiController : Controller
public async Task PostQuery([FromBody] HairMultiCutQuery query, CancellationToken cancellationToken)
{
var uid = User.GetUserId();
- if (string.IsNullOrWhiteSpace(query.ClientId))
- {
- query.ClientId = uid;
- }
+ query.ClientId = uid;
+ ModelState.Remove("Client");
ModelState.Remove("ClientId");
-
- if (query.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
- {
- ModelState.AddModelError("ClientId", "You can only create your own HairMultiCutQuery");
- return BadRequest(ModelState);
- }
+ ModelState.Remove("UserCreated");
+ ModelState.Remove("UserModified");
+ ModelState.Remove("SelectedProfile");
+ ModelState.Remove("PerformerProfile");
+ ModelState.Remove("Context");
+ ModelState.Remove("Regularization");
if (query.Prestations is null || query.Prestations.Count == 0)
{
diff --git a/src/Yavsc.Api/Controllers/Business/RdvQueryApiController.cs b/src/Yavsc.Api/Controllers/Business/RdvQueryApiController.cs
index fcfac2b22..48e3be2e1 100644
--- a/src/Yavsc.Api/Controllers/Business/RdvQueryApiController.cs
+++ b/src/Yavsc.Api/Controllers/Business/RdvQueryApiController.cs
@@ -65,18 +65,18 @@ public class RdvQueryApiController : Controller
public async Task PostQuery([FromBody] RdvQuery query, CancellationToken cancellationToken)
{
var uid = User.GetUserId();
- if (string.IsNullOrWhiteSpace(query.ClientId))
- {
- query.ClientId = uid;
- }
+ // Security: the caller always posts for themselves.
+ query.ClientId = uid;
+ query.EventDate = EnsureUtc(query.EventDate);
+ ModelState.Remove("Client");
ModelState.Remove("ClientId");
-
- if (query.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
- {
- ModelState.AddModelError("ClientId", "You can only create your own RdvQuery");
- return BadRequest(ModelState);
- }
+ ModelState.Remove("UserCreated");
+ ModelState.Remove("UserModified");
+ ModelState.Remove("SelectedProfile");
+ ModelState.Remove("PerformerProfile");
+ ModelState.Remove("Context");
+ ModelState.Remove("Regularization");
if (!ModelState.IsValid)
{
@@ -123,23 +123,44 @@ public class RdvQueryApiController : Controller
[HttpPut("{id}")]
public async Task PutQuery([FromRoute] long id, [FromBody] RdvQuery query, CancellationToken cancellationToken)
{
- if (!ModelState.IsValid)
- {
- return BadRequest(ModelState);
- }
-
- if (id != query.Id)
- {
- return BadRequest();
- }
-
var uid = User.GetUserId();
- if (query.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
+ var existing = await _context.RdvQueries
+ .Include(q => q.Location)
+ .SingleOrDefaultAsync(q => q.Id == id, cancellationToken);
+
+ if (existing is null)
+ {
+ return NotFound();
+ }
+
+ if (existing.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
{
return Forbid();
}
- _context.Entry(query).State = EntityState.Modified;
+ existing.ActivityCode = query.ActivityCode;
+ existing.PerformerId = query.PerformerId;
+ existing.Consent = query.Consent;
+ existing.EventDate = EnsureUtc(query.EventDate);
+ existing.LocationType = query.LocationType;
+ existing.Reason = query.Reason;
+ existing.Status = query.Status;
+ existing.Provisional = query.Provisional;
+
+ if (query.Location is not null)
+ {
+ var resolvedLocation = await _context.Locations.FirstOrDefaultAsync(
+ x => x.Address == query.Location.Address
+ && x.Longitude == query.Location.Longitude
+ && x.Latitude == query.Location.Latitude,
+ cancellationToken);
+
+ existing.Location = resolvedLocation ?? query.Location;
+ if (resolvedLocation is null)
+ {
+ _context.Attach(query.Location);
+ }
+ }
try
{
@@ -186,4 +207,14 @@ public class RdvQueryApiController : Controller
{
return _context.RdvQueries.Any(e => e.Id == id);
}
+
+ private static DateTime EnsureUtc(DateTime value)
+ {
+ return value.Kind switch
+ {
+ DateTimeKind.Utc => value,
+ DateTimeKind.Local => value.ToUniversalTime(),
+ _ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
+ };
+ }
}
diff --git a/src/Yavsc.Blogs.Tests/Fixtures/BlogsWebServerFixture.cs b/src/Yavsc.Blogs.Tests/Fixtures/BlogsWebServerFixture.cs
index 6e8ae31f6..ea6837b1c 100644
--- a/src/Yavsc.Blogs.Tests/Fixtures/BlogsWebServerFixture.cs
+++ b/src/Yavsc.Blogs.Tests/Fixtures/BlogsWebServerFixture.cs
@@ -370,8 +370,6 @@ public sealed class BlogsWebServerFixture : WebHostFixture
}
}
-
-
/// Create a circle owned by
/// directly in the SQLite store and return its server-assigned
/// id.
@@ -415,5 +413,4 @@ public sealed class BlogsWebServerFixture : WebHostFixture
db.SaveChanges();
return post.Id;
}
-
}
diff --git a/src/Yavsc.Blogs/Controllers/BlogApiController.cs b/src/Yavsc.Blogs/Controllers/BlogApiController.cs
index 8c76f7eea..b757d711b 100644
--- a/src/Yavsc.Blogs/Controllers/BlogApiController.cs
+++ b/src/Yavsc.Blogs/Controllers/BlogApiController.cs
@@ -55,6 +55,14 @@ namespace Yavsc.Blogs.Controllers
[HttpPut("{id}")]
public async Task PutBlog(long id, [FromBody] Models.Blog.BlogPost blog)
{
+ // These properties are server-managed or optional graph members and
+ // should not block JSON payloads coming from API clients.
+ ModelState.Remove(nameof(Models.Blog.BlogPost.Author));
+ ModelState.Remove(nameof(Models.Blog.BlogPost.Tags));
+ ModelState.Remove(nameof(Models.Blog.BlogPost.Comments));
+ ModelState.Remove(nameof(Models.Blog.BlogPost.UserCreated));
+ ModelState.Remove(nameof(Models.Blog.BlogPost.UserModified));
+
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
@@ -87,6 +95,14 @@ namespace Yavsc.Blogs.Controllers
[HttpPost]
public IActionResult PostBlog([FromBody] Models.Blog.BlogPost blog)
{
+ // These properties are server-managed or optional graph members and
+ // should not block JSON payloads coming from API clients.
+ ModelState.Remove(nameof(Models.Blog.BlogPost.Author));
+ ModelState.Remove(nameof(Models.Blog.BlogPost.Tags));
+ ModelState.Remove(nameof(Models.Blog.BlogPost.Comments));
+ ModelState.Remove(nameof(Models.Blog.BlogPost.UserCreated));
+ ModelState.Remove(nameof(Models.Blog.BlogPost.UserModified));
+
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
diff --git a/src/Yavsc.Org/Controllers/Accounting/AccountController.cs b/src/Yavsc.Org/Controllers/Accounting/AccountController.cs
index 77e0e32a4..3fd8a0159 100644
--- a/src/Yavsc.Org/Controllers/Accounting/AccountController.cs
+++ b/src/Yavsc.Org/Controllers/Accounting/AccountController.cs
@@ -25,6 +25,7 @@ using IdentityModel;
using Yavsc.Server.Helpers;
using Microsoft.AspNetCore.Mvc.Localization;
using System.Diagnostics;
+using System.Data.Common;
namespace Yavsc.Controllers
{
@@ -467,8 +468,25 @@ IHtmlLocalizerFactory htmlLocalizerFactory,
if (ModelState.IsValid)
{
+ var existingUser = await _userManager.FindByEmailAsync(model.Email);
+ if (existingUser is not null)
+ {
+ ModelState.AddModelError(nameof(model.Email), _localizer["DuplicateEmail"]);
+ return View(model);
+ }
+
var user = new ApplicationUser { UserName = model.UserName, Email = model.Email };
- var result = await _userManager.CreateAsync(user, model.Password);
+ IdentityResult result;
+ try
+ {
+ result = await _userManager.CreateAsync(user, model.Password);
+ }
+ catch (DbUpdateException ex) when (IsDuplicateEmailViolation(ex))
+ {
+ _logger.LogWarning(ex, "Registration rejected: duplicate email '{Email}'.", model.Email);
+ ModelState.AddModelError(nameof(model.Email), _localizer["DuplicateEmail"]);
+ return View(model);
+ }
if (result.Succeeded)
{
_logger.LogInformation(3, "User created a new account with password.");
@@ -517,6 +535,21 @@ IHtmlLocalizerFactory htmlLocalizerFactory,
return View(model);
}
+ private static bool IsDuplicateEmailViolation(Exception exception)
+ {
+ for (var current = exception; current is not null; current = current.InnerException)
+ {
+ if (current is DbException pg
+ && pg.ErrorCode == 23505)
+ // UNIQUE VIOLATION https://www.postgresql.org/docs/8.4/errcodes-appendix.html
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
[Authorize, HttpPost, ValidateAntiForgeryToken]
public async Task SendConfirationEmail()
{
diff --git a/src/Yavsc.Org/Controllers/Communicating/CommentsController.cs b/src/Yavsc.Org/Controllers/Communicating/CommentsController.cs
index 3690671c5..02dda32a1 100644
--- a/src/Yavsc.Org/Controllers/Communicating/CommentsController.cs
+++ b/src/Yavsc.Org/Controllers/Communicating/CommentsController.cs
@@ -121,6 +121,7 @@ namespace Yavsc.Controllers
public async Task Create(Comment comment)
{
comment.UserCreated = User.GetUserId();
+ comment.UserModified = comment.UserCreated;
// AuthorId/UserCreated is set server-side after model binding;
// remove the stale binding error so a valid authenticated POST
// does not fall into the invalid branch.
@@ -129,7 +130,7 @@ namespace Yavsc.Controllers
if (ModelState.IsValid)
{
_context.Comment.Add(comment);
- await _context.SaveChangesAsync();
+ await _context.SaveChangesAsync(comment.UserCreated);
return RedirectToAction("Index");
}
ViewBag.ReceiverId = new SelectList(_context.BlogSpot, "Id", "Title", comment.ReceiverId);
diff --git a/src/Yavsc.Org/Migrations/20260907091755_AddCircleAuthorizationToFileAcl.Designer.cs b/src/Yavsc.Org/Migrations/20260907091755_AddCircleAuthorizationToFileAcl.Designer.cs
new file mode 100644
index 000000000..096cf52f8
--- /dev/null
+++ b/src/Yavsc.Org/Migrations/20260907091755_AddCircleAuthorizationToFileAcl.Designer.cs
@@ -0,0 +1,4778 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using Yavsc.Models;
+
+#nullable disable
+
+namespace Yavsc.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20260907091755_AddCircleAuthorizationToFileAcl")]
+ partial class AddCircleAuthorizationToFileAcl
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.9")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResource", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id"));
+
+ b.Property("AllowedAccessTokenSigningAlgorithms")
+ .HasColumnType("text");
+
+ b.Property("Created")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("DisplayName")
+ .HasColumnType("text");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean");
+
+ b.Property("LastAccessed")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("NonEditable")
+ .HasColumnType("boolean");
+
+ b.Property("ShowInDiscoveryDocument")
+ .HasColumnType("boolean");
+
+ b.Property("Updated")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.ToTable("ApiResources");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ApiResourceId")
+ .HasColumnType("integer");
+
+ b.Property("ApiResourceId1")
+ .HasColumnType("integer");
+
+ b.Property("Type")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ApiResourceId");
+
+ b.HasIndex("ApiResourceId1");
+
+ b.ToTable("ApiResourceClaims");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceProperty", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ApiResourceId")
+ .HasColumnType("integer");
+
+ b.Property("ApiResourceId1")
+ .HasColumnType("integer");
+
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ApiResourceId");
+
+ b.HasIndex("ApiResourceId1");
+
+ b.ToTable("ApiResourceProperties");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceScope", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ApiResourceId")
+ .HasColumnType("integer");
+
+ b.Property("ApiResourceId1")
+ .HasColumnType("integer");
+
+ b.Property("Scope")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ApiResourceId");
+
+ b.HasIndex("ApiResourceId1");
+
+ b.ToTable("ApiResourceScopes");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceSecret", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ApiResourceId")
+ .HasColumnType("integer");
+
+ b.Property("ApiResourceId1")
+ .HasColumnType("integer");
+
+ b.Property("Created")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("Expiration")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Type")
+ .HasColumnType("text");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ApiResourceId");
+
+ b.HasIndex("ApiResourceId1");
+
+ b.ToTable("ApiResourceSecrets");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScope", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id"));
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("DisplayName")
+ .HasColumnType("text");
+
+ b.Property("Emphasize")
+ .HasColumnType("boolean");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("Required")
+ .HasColumnType("boolean");
+
+ b.Property("ShowInDiscoveryDocument")
+ .HasColumnType("boolean");
+
+ b.HasKey("Id");
+
+ b.ToTable("ApiScopes");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ScopeId")
+ .HasColumnType("integer");
+
+ b.Property("ScopeId1")
+ .HasColumnType("integer");
+
+ b.Property("Type")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ScopeId");
+
+ b.HasIndex("ScopeId1");
+
+ b.ToTable("ApiScopeClaims");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeProperty", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("ScopeId")
+ .HasColumnType("integer");
+
+ b.Property("ScopeId1")
+ .HasColumnType("integer");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ScopeId");
+
+ b.HasIndex("ScopeId1");
+
+ b.ToTable("ApiScopeProperties");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.Client", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id"));
+
+ b.Property("AbsoluteRefreshTokenLifetime")
+ .HasColumnType("integer");
+
+ b.Property("AccessTokenLifetime")
+ .HasColumnType("integer");
+
+ b.Property("AccessTokenType")
+ .HasColumnType("integer");
+
+ b.Property("AllowAccessTokensViaBrowser")
+ .HasColumnType("boolean");
+
+ b.Property("AllowOfflineAccess")
+ .HasColumnType("boolean");
+
+ b.Property("AllowPlainTextPkce")
+ .HasColumnType("boolean");
+
+ b.Property("AllowRememberConsent")
+ .HasColumnType("boolean");
+
+ b.Property("AllowedIdentityTokenSigningAlgorithms")
+ .HasColumnType("text");
+
+ b.Property("AlwaysIncludeUserClaimsInIdToken")
+ .HasColumnType("boolean");
+
+ b.Property("AlwaysSendClientClaims")
+ .HasColumnType("boolean");
+
+ b.Property("AuthorizationCodeLifetime")
+ .HasColumnType("integer");
+
+ b.Property("BackChannelLogoutSessionRequired")
+ .HasColumnType("boolean");
+
+ b.Property("BackChannelLogoutUri")
+ .HasColumnType("text");
+
+ b.Property("ClientClaimsPrefix")
+ .HasColumnType("text");
+
+ b.Property("ClientId")
+ .HasColumnType("text");
+
+ b.Property("ClientName")
+ .HasColumnType("text");
+
+ b.Property("ClientUri")
+ .HasColumnType("text");
+
+ b.Property("ConsentLifetime")
+ .HasColumnType("integer");
+
+ b.Property("Created")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("DeviceCodeLifetime")
+ .HasColumnType("integer");
+
+ b.Property("EnableLocalLogin")
+ .HasColumnType("boolean");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean");
+
+ b.Property("FrontChannelLogoutSessionRequired")
+ .HasColumnType("boolean");
+
+ b.Property("FrontChannelLogoutUri")
+ .HasColumnType("text");
+
+ b.Property("IdentityTokenLifetime")
+ .HasColumnType("integer");
+
+ b.Property("IncludeJwtId")
+ .HasColumnType("boolean");
+
+ b.Property("LastAccessed")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LogoUri")
+ .HasColumnType("text");
+
+ b.Property("NonEditable")
+ .HasColumnType("boolean");
+
+ b.Property("PairWiseSubjectSalt")
+ .HasColumnType("text");
+
+ b.Property("ProtocolType")
+ .HasColumnType("text");
+
+ b.Property("RefreshTokenExpiration")
+ .HasColumnType("integer");
+
+ b.Property("RefreshTokenUsage")
+ .HasColumnType("integer");
+
+ b.Property("RequireClientSecret")
+ .HasColumnType("boolean");
+
+ b.Property("RequireConsent")
+ .HasColumnType("boolean");
+
+ b.Property("RequirePkce")
+ .HasColumnType("boolean");
+
+ b.Property("RequireRequestObject")
+ .HasColumnType("boolean");
+
+ b.Property("SlidingRefreshTokenLifetime")
+ .HasColumnType("integer");
+
+ b.Property("UpdateAccessTokenClaimsOnRefresh")
+ .HasColumnType("boolean");
+
+ b.Property("Updated")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserCodeType")
+ .HasColumnType("text");
+
+ b.Property("UserSsoLifetime")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("Clients");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id"));
+
+ b.Property("ClientId")
+ .HasColumnType("integer");
+
+ b.Property("Type")
+ .HasColumnType("text");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClientId");
+
+ b.ToTable("ClientClaims");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientCorsOrigin", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id"));
+
+ b.Property("ClientId")
+ .HasColumnType("integer");
+
+ b.Property("ClientId1")
+ .HasColumnType("integer");
+
+ b.Property("Origin")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClientId");
+
+ b.HasIndex("ClientId1");
+
+ b.ToTable("ClientCorsOrigins");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id"));
+
+ b.Property("ClientId")
+ .HasColumnType("integer");
+
+ b.Property("GrantType")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClientId");
+
+ b.ToTable("ClientGrantTypes");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientIdPRestriction", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id"));
+
+ b.Property("ClientId")
+ .HasColumnType("integer");
+
+ b.Property("ClientId1")
+ .HasColumnType("integer");
+
+ b.Property("Provider")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClientId");
+
+ b.HasIndex("ClientId1");
+
+ b.ToTable("ClientIdPRestrictions");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id"));
+
+ b.Property("ClientId")
+ .HasColumnType("integer");
+
+ b.Property("ClientId1")
+ .HasColumnType("integer");
+
+ b.Property("PostLogoutRedirectUri")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClientId");
+
+ b.HasIndex("ClientId1");
+
+ b.ToTable("ClientPostLogoutRedirectUris");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientProperty", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id"));
+
+ b.Property("ClientId")
+ .HasColumnType("integer");
+
+ b.Property("ClientId1")
+ .HasColumnType("integer");
+
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClientId");
+
+ b.HasIndex("ClientId1");
+
+ b.ToTable("ClientProperties");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id"));
+
+ b.Property("ClientId")
+ .HasColumnType("integer");
+
+ b.Property("RedirectUri")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClientId");
+
+ b.ToTable("ClientRedirectUris");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id"));
+
+ b.Property("ClientId")
+ .HasColumnType("integer");
+
+ b.Property("Scope")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClientId");
+
+ b.ToTable("ClientScopes");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientSecret", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id"));
+
+ b.Property("ClientId")
+ .HasColumnType("integer");
+
+ b.Property("ClientId1")
+ .HasColumnType("integer");
+
+ b.Property("Created")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("Expiration")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Type")
+ .HasColumnType("text");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClientId");
+
+ b.HasIndex("ClientId1");
+
+ b.ToTable("ClientSecrets");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.DeviceFlowCodes", b =>
+ {
+ b.Property("UserCode")
+ .HasColumnType("text");
+
+ b.Property("DeviceCode")
+ .HasColumnType("text");
+
+ b.Property("ClientId")
+ .HasColumnType("text");
+
+ b.Property("CreationTime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Data")
+ .HasColumnType("text");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("Expiration")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("SessionId")
+ .HasColumnType("text");
+
+ b.Property("SubjectId")
+ .HasColumnType("text");
+
+ b.HasKey("UserCode", "DeviceCode");
+
+ b.ToTable("DeviceFlowCodes");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResource", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Created")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("DisplayName")
+ .HasColumnType("text");
+
+ b.Property("Emphasize")
+ .HasColumnType("boolean");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("NonEditable")
+ .HasColumnType("boolean");
+
+ b.Property("Required")
+ .HasColumnType("boolean");
+
+ b.Property("ShowInDiscoveryDocument")
+ .HasColumnType("boolean");
+
+ b.Property("Updated")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.ToTable("IdentityResources");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("IdentityResourceId")
+ .HasColumnType("integer");
+
+ b.Property("Type")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IdentityResourceId");
+
+ b.ToTable("IdentityResourceClaims");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceProperty", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("IdentityResourceId")
+ .HasColumnType("integer");
+
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IdentityResourceId");
+
+ b.ToTable("IdentityResourceProperties");
+ });
+
+ modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.PersistedGrant", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("ClientId")
+ .HasColumnType("text");
+
+ b.Property("ConsumedTime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreationTime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Data")
+ .HasColumnType("text");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("Expiration")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("SessionId")
+ .HasColumnType("text");
+
+ b.Property("SubjectId")
+ .HasColumnType("text");
+
+ b.Property("Type")
+ .HasColumnType("text");
+
+ b.HasKey("Key");
+
+ b.ToTable("PersistedGrants");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("text");
+
+ b.Property("ProviderKey")
+ .HasColumnType("text");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("LoginProvider")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("Yavsc.Abstract.Identity.ClientProviderInfo", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("Avatar")
+ .HasColumnType("text");
+
+ b.Property("BillingAddressId")
+ .HasColumnType("bigint");
+
+ b.Property