diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9bef40e30..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
@@ -13,12 +66,14 @@
* [PostIt] Les avatars ne sont plus relies en string sur `Image.Source`: ils sont telecharges et lies en `Bitmap`.
* [Yavsc.Api.Client] `ActivityApiClient` accepte une base d'avatar dediee et construit les URLs avatar depuis l'autorite d'identification.
+* [PostIt] Les clients Activites/Billing utilisent maintenant `ApiUrl` en lecture dynamique: un changement via Parametres prend effet sans redemarrer l'application (apres sauvegarde et rafraichissement de la page).
* [PostIt] Le header de `MainPage` n'utilise plus `ScrollViewer`; remplacement par une barre de commandes basee sur `WrapPanel`.
* [PostIt] Alignement de la navigation blogs: renommage `PushMainPageAsync` -> `PushBlogsPageAsync` et ajustement de `HomePageViewModel`.
### Fixed
-néant
+* [PostIt.Android] Correction d'un 404 sur la page Activites au premier lancement: la configuration embarquee pointait `ApiUrl` vers le host Blogs au lieu de l'API metier.
+* [PostIt] Correction du bouton Sauver de la page Parametres: binding vers `SaveCommand` pour persister correctement `ApiUrl`/`BlogsApiUrl`.
## [1.0.8-rc11] - unstable
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/ActivitiesPageViewModelTests.cs b/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs
index 2e8d3e544..04cc0f314 100644
--- a/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs
+++ b/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs
@@ -39,6 +39,22 @@ public class ActivitiesPageViewModelTests
Assert.Equal("https://business.example/api/v1/billing/Rdv", api.Paths[3]);
}
+ [Fact]
+ public async Task ActivityApiClient_uses_updated_business_base_without_restart()
+ {
+ var api = new StubActivityApi();
+ var baseUrl = "https://business-a.example/api/v1/";
+ var client = new ActivityApiClient(api, () => baseUrl);
+
+ await client.GetCatalogAsync(ct: TestContext.Current.CancellationToken);
+
+ baseUrl = "https://business-b.example/api/v1/";
+ await client.GetUsersAsync("brush", TestContext.Current.CancellationToken);
+
+ Assert.Equal("https://business-a.example/api/v1/activity/catalog", api.Paths[0]);
+ Assert.Equal("https://business-b.example/api/v1/activity/brush/users", api.Paths[1]);
+ }
+
[Fact]
public async Task RefreshAsync_loads_first_activity_then_specialization_performers()
{
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/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/AddCircleMemberDialog.axaml b/src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml
index 8b2329886..a17f8574e 100644
--- a/src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml
+++ b/src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml
@@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PostIt.Views.AddCircleMemberDialog"
xmlns:vm="using:PostIt.ViewModels"
+ xmlns:postitControls="using:PostIt.Controls"
xmlns:services="using:PostIt.Services"
x:DataType="vm:AddCircleMemberDialogViewModel"
>
@@ -47,8 +48,8 @@
-
+
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/MainPage.axaml b/src/PostIt/PostIt/Views/MainPage.axaml
index 41ed7ece3..871a2f541 100644
--- a/src/PostIt/PostIt/Views/MainPage.axaml
+++ b/src/PostIt/PostIt/Views/MainPage.axaml
@@ -3,6 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:PostIt.ViewModels"
+ xmlns:postitControls="using:PostIt.Controls"
xmlns:models="using:Yavsc.Blogspot"
xmlns:views="using:PostIt.Views"
xmlns:AvaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
@@ -104,7 +105,8 @@
VerticalAlignment="Stretch">
-
+
diff --git a/src/PostIt/PostIt/Views/PostAclDialog.axaml b/src/PostIt/PostIt/Views/PostAclDialog.axaml
index 8552988e5..4af8c018e 100644
--- a/src/PostIt/PostIt/Views/PostAclDialog.axaml
+++ b/src/PostIt/PostIt/Views/PostAclDialog.axaml
@@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PostIt.Views.PostAclDialog"
xmlns:vm="using:PostIt.ViewModels"
+ xmlns:postitControls="using:PostIt.Controls"
xmlns:dtos="using:Yavsc.Api.Client.Dtos"
x:DataType="vm:PostAclDialogViewModel"
>
@@ -53,8 +54,8 @@
-
+
diff --git a/src/PostIt/PostIt/Views/SettingsPage.axaml b/src/PostIt/PostIt/Views/SettingsPage.axaml
index 611b89954..2973679b5 100644
--- a/src/PostIt/PostIt/Views/SettingsPage.axaml
+++ b/src/PostIt/PostIt/Views/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" />
-
+
-
+
diff --git a/src/PostIt/PostIt/Views/SettingsPage.axaml.cs b/src/PostIt/PostIt/Views/SettingsPage.axaml.cs
index 593dcfeeb..b50245919 100644
--- a/src/PostIt/PostIt/Views/SettingsPage.axaml.cs
+++ b/src/PostIt/PostIt/Views/SettingsPage.axaml.cs
@@ -6,10 +6,10 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
-using Avalonia.Media;
using Avalonia.Platform.Storage;
using Microsoft.Extensions.DependencyInjection;
using PostIt.Services;
+using PostIt.ViewModels;
namespace PostIt.Views;
public partial class SettingsPage: ContentPage
@@ -28,12 +28,12 @@ public partial class SettingsPage: ContentPage
{
var statusVersion = Interlocked.Increment(ref _avatarStatusVersion);
ChooseAvatarButton.IsEnabled = false;
- SetAvatarStatus("Sélection d'un fichier avatar...", Brushes.Gray);
+ SetAvatarStatus("Selection d'un fichier avatar...", StatusSeverity.Info);
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel is null)
{
- SetAvatarStatus("Impossible d'accéder à la fenêtre active.", Brushes.IndianRed);
+ SetAvatarStatus("Impossible d'acceder a la fenetre active.", StatusSeverity.Error);
ChooseAvatarButton.IsEnabled = true;
return;
}
@@ -54,27 +54,28 @@ public partial class SettingsPage: ContentPage
var file = files.FirstOrDefault();
if (file is null)
{
- SetAvatarStatus("Upload annulé.", Brushes.Gray);
+ SetAvatarStatus("Upload annule.", StatusSeverity.Warning);
ChooseAvatarButton.IsEnabled = true;
return;
}
try
{
- SetAvatarStatus("Upload avatar en cours...", Brushes.Gray);
+ SetAvatarStatus("Upload avatar en cours...", StatusSeverity.Info);
await using var stream = await file.OpenReadAsync();
var api = ((App)App.Current!).ServiceProvider!.GetRequiredService();
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/SignaturePage.axaml
index d868f2a48..021208b54 100644
--- a/src/PostIt/PostIt/Views/SignaturePage.axaml
+++ b/src/PostIt/PostIt/Views/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/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.Client/ActivityApiClient.cs b/src/Yavsc.Api.Client/ActivityApiClient.cs
index 52fad1a49..8199de9d4 100644
--- a/src/Yavsc.Api.Client/ActivityApiClient.cs
+++ b/src/Yavsc.Api.Client/ActivityApiClient.cs
@@ -19,19 +19,29 @@ public sealed class ActivityApiClient
private const string PathPrefix = "activity";
private readonly IYavscApiClient _api;
- private readonly Uri _baseAddress;
- private readonly Uri _avatarBaseAddress;
+ private readonly Func _businessBaseAddress;
+ private readonly Func _avatarBaseAddress;
public ActivityApiClient(IYavscApiClient api, string businessBaseAddress, string? avatarBaseAddress = null)
+ : this(
+ api,
+ () => businessBaseAddress,
+ () => avatarBaseAddress)
+ {
+ }
+
+ public ActivityApiClient(
+ IYavscApiClient api,
+ Func businessBaseAddress,
+ Func? avatarBaseAddress = null)
{
_api = api ?? throw new ArgumentNullException(nameof(api));
- if (string.IsNullOrEmpty(businessBaseAddress))
- throw new ArgumentException("Base address is required.", nameof(businessBaseAddress));
+ _businessBaseAddress = businessBaseAddress ?? throw new ArgumentNullException(nameof(businessBaseAddress));
+ _avatarBaseAddress = avatarBaseAddress ?? (() => null);
- _baseAddress = new Uri(businessBaseAddress, UriKind.Absolute);
- _avatarBaseAddress = string.IsNullOrWhiteSpace(avatarBaseAddress)
- ? _baseAddress
- : new Uri(avatarBaseAddress, UriKind.Absolute);
+ // Validate initial values early to fail fast on invalid setup.
+ _ = ResolveBusinessBaseAddress();
+ _ = ResolveAvatarBaseAddress();
}
public Task> GetCatalogAsync(
@@ -65,7 +75,7 @@ public sealed class ActivityApiClient
public string BuildAvatarXsUrl(string? userName)
{
- var siteRoot = new Uri(_avatarBaseAddress, "/");
+ var siteRoot = new Uri(ResolveAvatarBaseAddress(), "/");
if (string.IsNullOrWhiteSpace(userName))
{
@@ -75,5 +85,22 @@ public sealed class ActivityApiClient
return new Uri(siteRoot, $"avatars/{Uri.EscapeDataString(userName)}.xs.png").ToString();
}
- private string Absolute(string relativePath) => new Uri(_baseAddress, relativePath).ToString();
+ private string Absolute(string relativePath) => new Uri(ResolveBusinessBaseAddress(), relativePath).ToString();
+
+ private Uri ResolveBusinessBaseAddress()
+ {
+ var raw = _businessBaseAddress();
+ if (string.IsNullOrWhiteSpace(raw))
+ throw new InvalidOperationException("Business base address is required.");
+
+ return new Uri(raw, UriKind.Absolute);
+ }
+
+ private Uri ResolveAvatarBaseAddress()
+ {
+ var raw = _avatarBaseAddress();
+ return string.IsNullOrWhiteSpace(raw)
+ ? ResolveBusinessBaseAddress()
+ : new Uri(raw, UriKind.Absolute);
+ }
}
diff --git a/src/Yavsc.Api.Client/BillingApiClient.cs b/src/Yavsc.Api.Client/BillingApiClient.cs
index 35a6722d5..c3a0be103 100644
--- a/src/Yavsc.Api.Client/BillingApiClient.cs
+++ b/src/Yavsc.Api.Client/BillingApiClient.cs
@@ -20,15 +20,20 @@ public sealed class BillingApiClient
private const string PathPrefix = "billing";
private readonly IYavscApiClient _api;
- private readonly Uri _baseAddress;
+ private readonly Func _businessBaseAddress;
public BillingApiClient(IYavscApiClient api, string businessBaseAddress)
+ : this(api, () => businessBaseAddress)
+ {
+ }
+
+ public BillingApiClient(IYavscApiClient api, Func businessBaseAddress)
{
_api = api ?? throw new ArgumentNullException(nameof(api));
- if (string.IsNullOrWhiteSpace(businessBaseAddress))
- throw new ArgumentException("Base address is required.", nameof(businessBaseAddress));
+ _businessBaseAddress = businessBaseAddress ?? throw new ArgumentNullException(nameof(businessBaseAddress));
- _baseAddress = new Uri(businessBaseAddress, UriKind.Absolute);
+ // Validate initial value early to fail fast on invalid setup.
+ _ = ResolveBusinessBaseAddress();
}
public Task CreateAsync(string billingCode, object payload, CancellationToken ct = default)
@@ -121,7 +126,16 @@ public sealed class BillingApiClient
ct: ct);
}
- private string Absolute(string relativePath) => new Uri(_baseAddress, relativePath).ToString();
+ private string Absolute(string relativePath) => new Uri(ResolveBusinessBaseAddress(), relativePath).ToString();
+
+ private Uri ResolveBusinessBaseAddress()
+ {
+ var raw = _businessBaseAddress();
+ if (string.IsNullOrWhiteSpace(raw))
+ throw new InvalidOperationException("Business base address is required.");
+
+ return new Uri(raw, UriKind.Absolute);
+ }
private static BillingQueryDetailsDto MapRdv(RdvQueryResponse dto, string billingCode)
{
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/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..44927d461 100644
--- a/src/Yavsc.Org/Controllers/Accounting/AccountController.cs
+++ b/src/Yavsc.Org/Controllers/Accounting/AccountController.cs
@@ -467,8 +467,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 +534,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 PostgresException pg
+ && pg.SqlState == PostgresErrorCodes.UniqueViolation
+ && string.Equals(pg.ConstraintName, "AK_AspNetUsers_Email", StringComparison.Ordinal))
+ {
+ 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.Server/Config.cs b/src/Yavsc.Server/Config.cs
index 5d2bcdffb..e68546eff 100644
--- a/src/Yavsc.Server/Config.cs
+++ b/src/Yavsc.Server/Config.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using IdentityServer8;
using IdentityServer8.Models;
diff --git a/src/Yavsc.Server/Constants.cs b/src/Yavsc.Server/Constants.cs
index 4ded48eae..8859e5e69 100644
--- a/src/Yavsc.Server/Constants.cs
+++ b/src/Yavsc.Server/Constants.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
namespace Yavsc.Server
{
diff --git a/src/Yavsc.Server/Exceptions/AuthorizationFailureException.cs b/src/Yavsc.Server/Exceptions/AuthorizationFailureException.cs
index a3c5105ec..4a588dfd9 100644
--- a/src/Yavsc.Server/Exceptions/AuthorizationFailureException.cs
+++ b/src/Yavsc.Server/Exceptions/AuthorizationFailureException.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
namespace Yavsc.Server.Exceptions;
[Serializable]
diff --git a/src/Yavsc.Server/Exceptions/YavscInfrastructureException.cs b/src/Yavsc.Server/Exceptions/YavscInfrastructureException.cs
index 7888970c5..583872414 100644
--- a/src/Yavsc.Server/Exceptions/YavscInfrastructureException.cs
+++ b/src/Yavsc.Server/Exceptions/YavscInfrastructureException.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
namespace Yavsc.Services
{
[Serializable]
diff --git a/src/Yavsc.Server/Helpers/BillingHelpers.cs b/src/Yavsc.Server/Helpers/BillingHelpers.cs
index c10a76155..789b85a96 100644
--- a/src/Yavsc.Server/Helpers/BillingHelpers.cs
+++ b/src/Yavsc.Server/Helpers/BillingHelpers.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.Globalization;
using Yavsc.Billing;
using Yavsc.Models.Billing;
diff --git a/src/Yavsc.Server/Helpers/EstimateSignatureFileHelper.cs b/src/Yavsc.Server/Helpers/EstimateSignatureFileHelper.cs
index e2d40266d..8545c3a0a 100644
--- a/src/Yavsc.Server/Helpers/EstimateSignatureFileHelper.cs
+++ b/src/Yavsc.Server/Helpers/EstimateSignatureFileHelper.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.Security.Claims;
using System.Text;
using System.Text.Json;
diff --git a/src/Yavsc.Server/Helpers/FileSystemHelpers.cs b/src/Yavsc.Server/Helpers/FileSystemHelpers.cs
index 7d9d86ed0..29ada6b77 100644
--- a/src/Yavsc.Server/Helpers/FileSystemHelpers.cs
+++ b/src/Yavsc.Server/Helpers/FileSystemHelpers.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.Security.Claims;
diff --git a/src/Yavsc.Server/Helpers/HtmlHelpers.cs b/src/Yavsc.Server/Helpers/HtmlHelpers.cs
index 213ea369a..c23f05cd2 100644
--- a/src/Yavsc.Server/Helpers/HtmlHelpers.cs
+++ b/src/Yavsc.Server/Helpers/HtmlHelpers.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Yavsc.Models.Drawing;
diff --git a/src/Yavsc.Server/Helpers/PayPalHelpers.cs b/src/Yavsc.Server/Helpers/PayPalHelpers.cs
index 9d4074d46..51902c784 100644
--- a/src/Yavsc.Server/Helpers/PayPalHelpers.cs
+++ b/src/Yavsc.Server/Helpers/PayPalHelpers.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using Microsoft.Extensions.Logging;
using Yavsc.Models.Billing;
using Microsoft.AspNetCore.Http;
diff --git a/src/Yavsc.Server/Helpers/RequestHelper.cs b/src/Yavsc.Server/Helpers/RequestHelper.cs
index 99fe15248..6b85a5c31 100644
--- a/src/Yavsc.Server/Helpers/RequestHelper.cs
+++ b/src/Yavsc.Server/Helpers/RequestHelper.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.Net;
using System.Net.Http.Headers;
using Yavsc.Server.Model;
diff --git a/src/Yavsc.Server/Helpers/ServiceExtensions.cs b/src/Yavsc.Server/Helpers/ServiceExtensions.cs
index 06e877be8..1a69ca42f 100644
--- a/src/Yavsc.Server/Helpers/ServiceExtensions.cs
+++ b/src/Yavsc.Server/Helpers/ServiceExtensions.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
diff --git a/src/Yavsc.Server/Helpers/UserHelpers.cs b/src/Yavsc.Server/Helpers/UserHelpers.cs
index c3ee708d6..f0a30995f 100644
--- a/src/Yavsc.Server/Helpers/UserHelpers.cs
+++ b/src/Yavsc.Server/Helpers/UserHelpers.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using Yavsc.Models;
diff --git a/src/Yavsc.Server/Hubs/ChatHub.cs b/src/Yavsc.Server/Hubs/ChatHub.cs
index 4a99b507c..799f7441c 100644
--- a/src/Yavsc.Server/Hubs/ChatHub.cs
+++ b/src/Yavsc.Server/Hubs/ChatHub.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
//
// ChatHub.cs
//
diff --git a/src/Yavsc.Server/Hubs/HubInputValidator.cs b/src/Yavsc.Server/Hubs/HubInputValidator.cs
index a6acac5ac..a2382c11e 100644
--- a/src/Yavsc.Server/Hubs/HubInputValidator.cs
+++ b/src/Yavsc.Server/Hubs/HubInputValidator.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
//
// ChatHub.cs
//
diff --git a/src/Yavsc.Server/Models/Access/GrantViewModel.cs b/src/Yavsc.Server/Models/Access/GrantViewModel.cs
index d9d0e1562..d0d379caa 100644
--- a/src/Yavsc.Server/Models/Access/GrantViewModel.cs
+++ b/src/Yavsc.Server/Models/Access/GrantViewModel.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
diff --git a/src/Yavsc.Server/Models/ApplicationDbContext.cs b/src/Yavsc.Server/Models/ApplicationDbContext.cs
index cb08023a3..ade093c15 100644
--- a/src/Yavsc.Server/Models/ApplicationDbContext.cs
+++ b/src/Yavsc.Server/Models/ApplicationDbContext.cs
@@ -108,6 +108,7 @@ namespace Yavsc.Models
;
builder.Entity().Property(a => a.ParentCode).IsRequired(false);
+ builder.Entity().Property(a => a.Description).IsRequired(false);
builder.Entity().HasKey(c => c.Code);
builder.Entity()
diff --git a/src/Yavsc.Server/Models/ApplicationUser.cs b/src/Yavsc.Server/Models/ApplicationUser.cs
index 355e0ad15..69fbf1e52 100644
--- a/src/Yavsc.Server/Models/ApplicationUser.cs
+++ b/src/Yavsc.Server/Models/ApplicationUser.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Newtonsoft.Json;
diff --git a/src/Yavsc.Server/Models/Auth/DeviceDeclaration.cs b/src/Yavsc.Server/Models/Auth/DeviceDeclaration.cs
index 1db1d89e0..7ed5e9b0d 100644
--- a/src/Yavsc.Server/Models/Auth/DeviceDeclaration.cs
+++ b/src/Yavsc.Server/Models/Auth/DeviceDeclaration.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Newtonsoft.Json;
diff --git a/src/Yavsc.Server/Models/Bank/BankIdentity.cs b/src/Yavsc.Server/Models/Bank/BankIdentity.cs
index eea65d691..092059acc 100644
--- a/src/Yavsc.Server/Models/Bank/BankIdentity.cs
+++ b/src/Yavsc.Server/Models/Bank/BankIdentity.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
diff --git a/src/Yavsc.Server/Models/Billing/Estimate.cs b/src/Yavsc.Server/Models/Billing/Estimate.cs
index e6495a140..24cd021a8 100644
--- a/src/Yavsc.Server/Models/Billing/Estimate.cs
+++ b/src/Yavsc.Server/Models/Billing/Estimate.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
diff --git a/src/Yavsc.Server/Models/Billing/NominativeServiceCommand.cs b/src/Yavsc.Server/Models/Billing/NominativeServiceCommand.cs
index ad651d368..aa9e06abf 100644
--- a/src/Yavsc.Server/Models/Billing/NominativeServiceCommand.cs
+++ b/src/Yavsc.Server/Models/Billing/NominativeServiceCommand.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
diff --git a/src/Yavsc.Server/Models/Billing/histoestim.cs b/src/Yavsc.Server/Models/Billing/histoestim.cs
index 1f9b99f1f..df2860f5d 100644
--- a/src/Yavsc.Server/Models/Billing/histoestim.cs
+++ b/src/Yavsc.Server/Models/Billing/histoestim.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
namespace Yavsc.Models.Billing
{
public partial class histoestim
diff --git a/src/Yavsc.Server/Models/Blog/BlogPost.cs b/src/Yavsc.Server/Models/Blog/BlogPost.cs
index 4c2f2ae5c..6dc8ff750 100644
--- a/src/Yavsc.Server/Models/Blog/BlogPost.cs
+++ b/src/Yavsc.Server/Models/Blog/BlogPost.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Newtonsoft.Json;
diff --git a/src/Yavsc.Server/Models/Blog/Comment.cs b/src/Yavsc.Server/Models/Blog/Comment.cs
index 4d39d0c2e..5d7cc2912 100644
--- a/src/Yavsc.Server/Models/Blog/Comment.cs
+++ b/src/Yavsc.Server/Models/Blog/Comment.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Newtonsoft.Json;
diff --git a/src/Yavsc.Server/Models/Calendar/Period.cs b/src/Yavsc.Server/Models/Calendar/Period.cs
index dfc285cf3..f333235b1 100644
--- a/src/Yavsc.Server/Models/Calendar/Period.cs
+++ b/src/Yavsc.Server/Models/Calendar/Period.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
//
// Period.cs
//
diff --git a/src/Yavsc.Server/Models/ErrorViewModel.cs b/src/Yavsc.Server/Models/ErrorViewModel.cs
index 178194769..087aa505b 100644
--- a/src/Yavsc.Server/Models/ErrorViewModel.cs
+++ b/src/Yavsc.Server/Models/ErrorViewModel.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
namespace Yavsc.Models;
public class ErrorViewModel
diff --git a/src/Yavsc.Server/Models/HairCut/BrusherProfile.cs b/src/Yavsc.Server/Models/HairCut/BrusherProfile.cs
index ba47bfdce..d89a56fa8 100644
--- a/src/Yavsc.Server/Models/HairCut/BrusherProfile.cs
+++ b/src/Yavsc.Server/Models/HairCut/BrusherProfile.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
//
// BrusherProfile.cs
diff --git a/src/Yavsc.Server/Models/HairCut/HairCutPaymentEvent.cs b/src/Yavsc.Server/Models/HairCut/HairCutPaymentEvent.cs
index 353249288..2c96a7d80 100644
--- a/src/Yavsc.Server/Models/HairCut/HairCutPaymentEvent.cs
+++ b/src/Yavsc.Server/Models/HairCut/HairCutPaymentEvent.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using Microsoft.Extensions.Localization;
using Yavsc.Interfaces.Workflow;
diff --git a/src/Yavsc.Server/Models/HairCut/HairCutQuery.cs b/src/Yavsc.Server/Models/HairCut/HairCutQuery.cs
index 0960db6d6..680cf6956 100644
--- a/src/Yavsc.Server/Models/HairCut/HairCutQuery.cs
+++ b/src/Yavsc.Server/Models/HairCut/HairCutQuery.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Yavsc.Models.Billing;
diff --git a/src/Yavsc.Server/Models/HairCut/HairCutQueryEvent.cs b/src/Yavsc.Server/Models/HairCut/HairCutQueryEvent.cs
index bd30fb784..fadc353a8 100644
--- a/src/Yavsc.Server/Models/HairCut/HairCutQueryEvent.cs
+++ b/src/Yavsc.Server/Models/HairCut/HairCutQueryEvent.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using Yavsc.Interfaces.Workflow;
namespace Yavsc.Models.Haircut
diff --git a/src/Yavsc.Server/Models/HairCut/HairMultiCutQuery.cs b/src/Yavsc.Server/Models/HairCut/HairMultiCutQuery.cs
index 21ae437ce..beb7da9ac 100644
--- a/src/Yavsc.Server/Models/HairCut/HairMultiCutQuery.cs
+++ b/src/Yavsc.Server/Models/HairCut/HairMultiCutQuery.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Yavsc.Models.Billing;
diff --git a/src/Yavsc.Server/Models/HairCut/HairPrestation.cs b/src/Yavsc.Server/Models/HairCut/HairPrestation.cs
index 6153f6483..2e2cf8ca6 100644
--- a/src/Yavsc.Server/Models/HairCut/HairPrestation.cs
+++ b/src/Yavsc.Server/Models/HairCut/HairPrestation.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
diff --git a/src/Yavsc.Server/Models/HairCut/Views/HaircutQueryInfo.cs b/src/Yavsc.Server/Models/HairCut/Views/HaircutQueryInfo.cs
index c9213862e..4099dbcaa 100644
--- a/src/Yavsc.Server/Models/HairCut/Views/HaircutQueryInfo.cs
+++ b/src/Yavsc.Server/Models/HairCut/Views/HaircutQueryInfo.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
//
// HaircutQueryInfo.cs
diff --git a/src/Yavsc.Server/Models/IT/SourceCode/GitClone.cs b/src/Yavsc.Server/Models/IT/SourceCode/GitClone.cs
index 826e5de38..d58551a65 100644
--- a/src/Yavsc.Server/Models/IT/SourceCode/GitClone.cs
+++ b/src/Yavsc.Server/Models/IT/SourceCode/GitClone.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
// // GitClone.cs
// /*
// paul 21/06/2018 11:27 20182018 6 21
diff --git a/src/Yavsc.Server/Models/IT/SourceCode/ProjectBuild.cs b/src/Yavsc.Server/Models/IT/SourceCode/ProjectBuild.cs
index f65b17b51..eb1afe214 100644
--- a/src/Yavsc.Server/Models/IT/SourceCode/ProjectBuild.cs
+++ b/src/Yavsc.Server/Models/IT/SourceCode/ProjectBuild.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.Diagnostics;
namespace Yavsc.Server.Models.IT.SourceCode
diff --git a/src/Yavsc.Server/Models/IT/SourceCode/SingleCmdProjectBatch.cs b/src/Yavsc.Server/Models/IT/SourceCode/SingleCmdProjectBatch.cs
index 2ce162564..0fb2116f5 100644
--- a/src/Yavsc.Server/Models/IT/SourceCode/SingleCmdProjectBatch.cs
+++ b/src/Yavsc.Server/Models/IT/SourceCode/SingleCmdProjectBatch.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.Diagnostics;
namespace Yavsc.Server.Models.IT.SourceCode
diff --git a/src/Yavsc.Server/Models/IdentityUserLogin.cs b/src/Yavsc.Server/Models/IdentityUserLogin.cs
index bfbd4e899..78567f68f 100644
--- a/src/Yavsc.Server/Models/IdentityUserLogin.cs
+++ b/src/Yavsc.Server/Models/IdentityUserLogin.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
namespace Yavsc.Models.Auth
{
using System.ComponentModel.DataAnnotations.Schema;
diff --git a/src/Yavsc.Server/Models/Kyc/TrustDeclaration.cs b/src/Yavsc.Server/Models/Kyc/TrustDeclaration.cs
index 0a97eb6a9..fe9fc2ec4 100644
--- a/src/Yavsc.Server/Models/Kyc/TrustDeclaration.cs
+++ b/src/Yavsc.Server/Models/Kyc/TrustDeclaration.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
// Yavsc.Models.Kyc/TrustDeclaration.cs
namespace Yavsc.Models.Kyc
{
diff --git a/src/Yavsc.Server/Models/Kyc/TrustToken.cs b/src/Yavsc.Server/Models/Kyc/TrustToken.cs
index 2e240bae1..8c09d39cd 100644
--- a/src/Yavsc.Server/Models/Kyc/TrustToken.cs
+++ b/src/Yavsc.Server/Models/Kyc/TrustToken.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
// Yavsc.Models.Kyc/TrustToken.cs
namespace Yavsc.Models.Kyc
{
diff --git a/src/Yavsc.Server/Models/Market/Product.cs b/src/Yavsc.Server/Models/Market/Product.cs
index ae8f29199..84f5709ca 100644
--- a/src/Yavsc.Server/Models/Market/Product.cs
+++ b/src/Yavsc.Server/Models/Market/Product.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
diff --git a/src/Yavsc.Server/Models/Relationship/StaticContact.cs b/src/Yavsc.Server/Models/Relationship/StaticContact.cs
index 7ae6ab6ae..05ebf04da 100644
--- a/src/Yavsc.Server/Models/Relationship/StaticContact.cs
+++ b/src/Yavsc.Server/Models/Relationship/StaticContact.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
namespace Yavsc.Models.Relationship
{
diff --git a/src/Yavsc.Server/Models/Workflow/Activity.cs b/src/Yavsc.Server/Models/Workflow/Activity.cs
index 0f813a273..e82ac9709 100644
--- a/src/Yavsc.Server/Models/Workflow/Activity.cs
+++ b/src/Yavsc.Server/Models/Workflow/Activity.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Newtonsoft.Json;
@@ -35,7 +37,7 @@ namespace Yavsc.Models.Workflow
public virtual List Children { get; set; }
[Display(Name = "Description")]
- public string Description { get; set; }
+ public string? Description { get; set; }
[Display(Name = "Photo")]
public string? Photo { get; set; }
diff --git a/src/Yavsc.Server/Models/Workflow/PerformerProfile.cs b/src/Yavsc.Server/Models/Workflow/PerformerProfile.cs
index 23bd688b4..c2f312b84 100644
--- a/src/Yavsc.Server/Models/Workflow/PerformerProfile.cs
+++ b/src/Yavsc.Server/Models/Workflow/PerformerProfile.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
diff --git a/src/Yavsc.Server/Models/Workflow/RdvQuery.cs b/src/Yavsc.Server/Models/Workflow/RdvQuery.cs
index f821fc75d..cd56f3676 100644
--- a/src/Yavsc.Server/Models/Workflow/RdvQuery.cs
+++ b/src/Yavsc.Server/Models/Workflow/RdvQuery.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
diff --git a/src/Yavsc.Server/Models/societe.com/CompanyInfo.cs b/src/Yavsc.Server/Models/societe.com/CompanyInfo.cs
index 114c4d8e3..354bc4861 100644
--- a/src/Yavsc.Server/Models/societe.com/CompanyInfo.cs
+++ b/src/Yavsc.Server/Models/societe.com/CompanyInfo.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
diff --git a/src/Yavsc.Server/Services/BillingService.cs b/src/Yavsc.Server/Services/BillingService.cs
index ad9361767..1a0bc199d 100644
--- a/src/Yavsc.Server/Services/BillingService.cs
+++ b/src/Yavsc.Server/Services/BillingService.cs
@@ -1,6 +1,7 @@
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Yavsc.Abstract.Workflow;
+using Yavsc.Helpers;
using Yavsc.Models;
namespace Yavsc.Services
@@ -35,7 +36,34 @@ namespace Yavsc.Services
public static IQuery GetBillable(ApplicationDbContext context, string billingCode, long queryId)
{
- throw new NotImplementedException();
+ if (context is null) throw new ArgumentNullException(nameof(context));
+ if (string.IsNullOrWhiteSpace(billingCode) || queryId <= 0)
+ {
+ return null;
+ }
+
+ if (Billing.Count == 0)
+ {
+ WorkflowHelpers.ConfigureBillingService();
+ }
+
+ var getter = Billing
+ .FirstOrDefault(kvp => string.Equals(kvp.Key, billingCode.Trim(), StringComparison.OrdinalIgnoreCase))
+ .Value;
+
+ if (getter is null)
+ {
+ return null;
+ }
+
+ try
+ {
+ return getter(context, queryId);
+ }
+ catch (InvalidOperationException)
+ {
+ return null;
+ }
}
diff --git a/src/Yavsc.Server/Services/BlogSpotService.cs b/src/Yavsc.Server/Services/BlogSpotService.cs
index a08326303..20f7245e5 100644
--- a/src/Yavsc.Server/Services/BlogSpotService.cs
+++ b/src/Yavsc.Server/Services/BlogSpotService.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.Diagnostics;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
diff --git a/src/Yavsc.Server/Services/ClaudeModerationService.cs b/src/Yavsc.Server/Services/ClaudeModerationService.cs
index a510e55cb..a47952318 100644
--- a/src/Yavsc.Server/Services/ClaudeModerationService.cs
+++ b/src/Yavsc.Server/Services/ClaudeModerationService.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using Yavsc.Moderation;
using Yavsc.Abstract.Interfaces;
using Anthropic.SDK;
diff --git a/src/Yavsc.Server/Services/LiveProcessor.cs b/src/Yavsc.Server/Services/LiveProcessor.cs
index f10a8201a..560ee6d0a 100644
--- a/src/Yavsc.Server/Services/LiveProcessor.cs
+++ b/src/Yavsc.Server/Services/LiveProcessor.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.Collections.Concurrent;
using System.Net.WebSockets;
using Yavsc.Models;
diff --git a/src/Yavsc.Server/Services/MailSender.cs b/src/Yavsc.Server/Services/MailSender.cs
index 592a43ba6..81518eddb 100644
--- a/src/Yavsc.Server/Services/MailSender.cs
+++ b/src/Yavsc.Server/Services/MailSender.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.Extensions.Logging;
diff --git a/src/Yavsc.Server/Services/PermissionHandler.cs b/src/Yavsc.Server/Services/PermissionHandler.cs
index 6848070ac..77ee9fd57 100644
--- a/src/Yavsc.Server/Services/PermissionHandler.cs
+++ b/src/Yavsc.Server/Services/PermissionHandler.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
diff --git a/src/Yavsc.Server/Services/ProfileService.cs b/src/Yavsc.Server/Services/ProfileService.cs
index b6da3a568..3b7b5121a 100644
--- a/src/Yavsc.Server/Services/ProfileService.cs
+++ b/src/Yavsc.Server/Services/ProfileService.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.Security.Claims;
using IdentityModel;
using IdentityServer8.Models;
diff --git a/src/Yavsc.Server/Settings/SiteSettings.cs b/src/Yavsc.Server/Settings/SiteSettings.cs
index ef74781de..3d604f79f 100644
--- a/src/Yavsc.Server/Settings/SiteSettings.cs
+++ b/src/Yavsc.Server/Settings/SiteSettings.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using Yavsc.Models.Relationship;
namespace Yavsc
diff --git a/src/Yavsc.Server/ViewModels/Account/ForgotPasswordViewModel.cs b/src/Yavsc.Server/ViewModels/Account/ForgotPasswordViewModel.cs
index 6af77fe4d..903d4c8b3 100644
--- a/src/Yavsc.Server/ViewModels/Account/ForgotPasswordViewModel.cs
+++ b/src/Yavsc.Server/ViewModels/Account/ForgotPasswordViewModel.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
diff --git a/src/Yavsc.Server/ViewModels/Account/SignInModel.cs b/src/Yavsc.Server/ViewModels/Account/SignInModel.cs
old mode 100755
new mode 100644
index d2aa69ad2..6375c40fe
--- a/src/Yavsc.Server/ViewModels/Account/SignInModel.cs
+++ b/src/Yavsc.Server/ViewModels/Account/SignInModel.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
namespace Yavsc.ViewModels.Account
diff --git a/src/Yavsc.Server/ViewModels/Account/UnregisterViewModel.cs b/src/Yavsc.Server/ViewModels/Account/UnregisterViewModel.cs
index e28fcfed8..e08330440 100644
--- a/src/Yavsc.Server/ViewModels/Account/UnregisterViewModel.cs
+++ b/src/Yavsc.Server/ViewModels/Account/UnregisterViewModel.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
namespace Yavsc.ViewModels.Account
diff --git a/src/Yavsc.Server/ViewModels/Account/VerifyCodeViewModel.cs b/src/Yavsc.Server/ViewModels/Account/VerifyCodeViewModel.cs
index a768e249f..5252ff4f6 100644
--- a/src/Yavsc.Server/ViewModels/Account/VerifyCodeViewModel.cs
+++ b/src/Yavsc.Server/ViewModels/Account/VerifyCodeViewModel.cs
@@ -1,3 +1,5 @@
+#nullable enable annotations
+
using System.ComponentModel.DataAnnotations;
namespace Yavsc.ViewModels.Account