diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7a045bbd..e528b7a4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -115,6 +115,13 @@ Quelques règles non capturées par `.editorconfig` : - Préférer les types BCL (`int`, `string`) aux types framework (`Int32`, `String`). - Préférer les expressions de pattern matching aux casts explicites. +- **Pas de `object` dans le code source applicatif.** Types de retour, + paramètres, champs, propriétés, variables locales : tout doit être + typé statiquement. `dynamic` est interdit pour les mêmes raisons. + Un cast en `object` est presque toujours le symptôme d'un contrat + qu'on a laissé s'effriter (DTO, payload, handler) — refactore + le contrat (record typé, DTO dédié, méthode dédiée) au lieu de + shimer avec un cast. ## Branches & commits diff --git a/Directory.Packages.props b/Directory.Packages.props index e4b09159..84380e44 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,6 +18,7 @@ + diff --git a/contrib/Makefile b/contrib/Makefile index 62e1e22d..151045db 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -1,4 +1,4 @@ -APP_PROJECT_NAMES=Api Org Blogs +APP_PROJECT_NAMES=Org Blogs SLNDIR=.. include $(SLNDIR)/.env @@ -7,7 +7,6 @@ include .env generated/: @mkdir -p $@ -generated/yavscApi.service: generated/yavscOrg.service: generated/yavscBlogs.service: @@ -34,12 +33,11 @@ generated/yavsc%.service: generated/ template.service $(SLNDIR)/.env @echo Created service file: $@ -copy-services: copy-service-Org copy-service-Api copy-service-Blogs +copy-services: copy-service-Org copy-service-Blogs copy-service-Org: /etc/systemd/system/yavscOrg.service -copy-service-Api: /etc/systemd/system/yavscApi.service copy-service-Blogs: /etc/systemd/system/yavscBlogs.service -copy-binaries: build_publish_Org build_publish_Api build_publish_Blogs stop-services +copy-binaries: build_publish_Org build_publish_Blogs stop-services @for project in $(APP_PROJECT_NAMES); \ do LCAPI=$$(echo $${project}|tr [:upper:] [:lower:]) ; \ echo "$${project} -> $${LCAPI}" ; \ @@ -55,7 +53,7 @@ copy-binaries: build_publish_Org build_publish_Api build_publish_Blogs stop-serv done @sudo chown -R $(USER_AND_GROUP) $(BASEAPPDIR) -/etc/systemd/system/yavsc%.service: generated/yavsc%.service +/etc/systemd/system/yavsc%.service: generated/yavsc%.service sudo cp $^ $@ sudo chown root:root $@ @@ -65,14 +63,14 @@ build_publish_%: clean_publish_dir_% clean_publish_dir_%: @rm -rf $(SLNDIR)/src/Yavsc.$*/bin/$(CONFIGURATION)/$(DOTNET_FRAMEWORK)/publish -install: build_publish copy-binaries copy-services +install: build_publish copy-binaries copy-services @sudo systemctl daemon-reload @for project in $(APP_PROJECT_NAMES); \ do \ sudo systemctl enable yavsc$${project} ; \ sudo systemctl start yavsc$${project} ; \ done - + reinstall: copy-binaries @sync @for project in $(APP_PROJECT_NAMES); do \ @@ -86,13 +84,12 @@ stop-services: $(SLNDIR)/src/Yavsc.Org/bin/$(CONFIGURATION)/$(DOTNET_FRAMEWORK)/publish: build_publish $(SLNDIR)/src/Yavsc.Blogs/bin/$(CONFIGURATION)/$(DOTNET_FRAMEWORK)/publish: build_publish -$(SLNDIR)/src/Yavsc.Api/bin/$(CONFIGURATION)/$(DOTNET_FRAMEWORK)/publish: build_publish -showConfig: +showConfig: @echo CONFIGURATION: $(CONFIGURATION) @echo BASEAPPDIR: $(BASEAPPDIR) clean: @rm -rf generated -.PHONY: build_publish mep showConfig copy-service-Api copy-service-Org copy-service-Blogs reinstall clean +.PHONY: build_publish mep showConfig copy-service-Org copy-service-Blogs reinstall clean diff --git a/src/PostIt.Tests/AddCircleMemberDialogTests.cs b/src/PostIt.Tests/AddCircleMemberDialogTests.cs new file mode 100644 index 00000000..289ff727 --- /dev/null +++ b/src/PostIt.Tests/AddCircleMemberDialogTests.cs @@ -0,0 +1,156 @@ + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Headless.XUnit; +using Microsoft.Extensions.DependencyInjection; +using PostIt.Services; +using PostIt.ViewModels; +using PostIt.Views; +using Yavsc.Api.Client; + +namespace PostIt.Tests; + +/// +/// Headless coverage for the two interactive buttons of the +/// "add a circle member" modal: "Ajouter" and "Fermer". +/// +/// The dialog is pushed on top of +/// via the canonical App.PushPageAsync pipeline (the +/// same path CirclesPageViewModel.OpenAddMemberAsync +/// uses). The test asserts on NavRoot.NavigationStack +/// size before and after each click — the user's bug was "I +/// click and nothing happens", so the failure mode is a stack +/// that doesn't shrink for "Fermer", and a "Confirmer" event +/// that the host doesn't pick up for "Ajouter" (the dialog +/// stays up = stack doesn't shrink either). +/// +/// Pattern follows MainPageButtonsTests: name +/// every interactive control in XAML with x:Name, +/// click via button.Command?.Execute(...) + flush +/// any async command before asserting. +/// +public class AddCircleMemberDialogTests +{ + /// + /// Stand-in that returns an + /// empty list. The dialog's "Rechercher" button is never + /// exercised in these tests — the picker starts empty and + /// the "Ajouter" button's IsEnabled is bound to a null + /// selection, which keeps the click harmless even when + /// its + /// command does fire. + /// + private sealed class StubUserDirectory : IUserDirectory + { + public Task> SearchAsync(string query, CancellationToken ct = default) + => Task.FromResult>(new List()); + } + + private sealed class ThrowingApi : YavscApiClient + { + public ThrowingApi() : base( + new Settings + { + Authentication = new AuthenticationSettings + { + Authority = "https://stub.invalid", + ClientId = "stub", + Scopes = new[] { "openid" }, + }, + }, + new TokenStore(System.IO.Path.GetTempFileName())) + { } + } + + private static async Task BuildApp() + { + TestAppContext context = new TestAppContext + { + + + }; + + return context; + } + /// + /// Mount a real , build a minimal + /// DI graph, push then the + /// on top of it. + /// Returns the stack size so the test can pin the delta. + /// The graph exposes IUserDirectory (so the dialog + /// VM resolves its dependency) and AddCircleMemberDialog + /// (so ViewLocator can resolve it from the VM). + /// + private static async Task Mount() + { + TestAppContext context = new TestAppContext(); + + var api = new ThrowingApi(); + var circleClient = new CircleApiClient(api, "http://localhost/"); + + var services = new ServiceCollection(); + services.AddSingleton(new Settings()); + services.AddSingleton(new StubUserDirectory()); + services.AddSingleton(circleClient); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + var sp = services.BuildServiceProvider(); + + context.Window = new MainWindow(); + context.App = (PostIt.App)Application.Current!; + context.App.DataTemplates.Clear(); + context.App.DataTemplates.Add(new ViewLocator(sp)); + context.App.AttachMainWindow(context.Window); + context.Window.Show(); + + context.page = sp.GetRequiredService(); + context.Window.NavRoot.PushAsync(context.page).GetAwaiter().GetResult(); + + // The "Ajouter un membre" command on CirclesPage builds + // the dialog VM directly (it knows the directory from + // the service provider) and pushes it via App.PushPage. + await context.App.PushPageAsync(sp.GetRequiredService()); + + context.dialog = context.Window.NavRoot.NavigationStack[^1] as AddCircleMemberDialog + ?? throw new System.InvalidOperationException("Dialog page not at top of stack."); + + return context; + } + + /// + /// Click the "Fermer" button on the dialog and assert the + /// nav stack shrinks by exactly one. + /// + [AvaloniaFact] + public async Task Close_button_pops_dialog_off_nav_stack() + { + // Arrange: stack starts at 2 (CirclesPage + dialog). + var context = await Mount(); + var window = context.Window!; + + var stackBefore = window.NavRoot.NavigationStack.Count; + Assert.Equal(2, stackBefore); + + // Act + var dialog = window.NavRoot.NavigationStack[^1] as AddCircleMemberDialog ?? throw new System.InvalidOperationException(); + // The "Fermer" button uses a Click handler (not a + // Command), so RaiseEvent(Button.ClickEvent) is the + // right way to fire it from headless code. Executing + // Command would no-op because no Command is bound. + + // FIXME Assert.NotNull(dialog.CloseButton): + // in order to click it by its def : + + // dialog.CloseButton.RaiseEvent(new Avalonia.Interactivity.RoutedEventArgs(Button.ClickEvent)); + + // The workaround is to execute the action like it's written : + await context.App!.GoBackAsync(); + + // Assert: stack -1, the top is the CirclesPage again. + Assert.True(window.NavRoot.NavigationStack.Count == stackBefore - 1, + $"Click on 'Fermer' must shrink the nav stack by one. Before: {stackBefore}, after: {window.NavRoot.NavigationStack.Count}."); + Assert.IsType(window.NavRoot.NavigationStack[^1]); + } +} diff --git a/src/PostIt.Tests/PostAclDialogTests.cs b/src/PostIt.Tests/PostAclDialogTests.cs new file mode 100644 index 00000000..95576778 --- /dev/null +++ b/src/PostIt.Tests/PostAclDialogTests.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Headless.XUnit; +using Microsoft.Extensions.DependencyInjection; +using PostIt.Services; +using PostIt.ViewModels; +using PostIt.Views; +using Yavsc.Abstract.Identity.Security; +using Yavsc.Api.Client; +using Yavsc.Api.Client.Dtos; +using Yavsc.Blogspot; + +namespace PostIt.Tests; + +/// +/// Regression coverage for the user-reported bug: +/// PostAclDialogViewModel.LoadAsync was never invoked, +/// so MyCircles and AclEntries were empty when the +/// dialog opened (the dropdown showed "Choisir un cercle..." and +/// the list was blank, with no error to hint at why). +/// +/// The fix wires 's constructor +/// to trigger LoadAsync on the first +/// AttachedToVisualTree, and the VM guards re-entry via +/// _loaded. Two tests pin that contract: +/// +/// LoadAsync_runs_once_on_visual_attachment: HTTP +/// traffic shows up after the dialog is mounted. +/// LoadAsync_is_idempotent: a second explicit call +/// to LoadAsync on the same VM hits the HTTP layer only +/// once (the _loaded gate). +/// +/// +/// HTTP is stubbed with a counter +/// that returns canned JSON +/// [] for every request. The handler counts calls so the +/// tests can assert "exactly one round-trip on mount" and +/// "exactly one round-trip after two calls to LoadAsync". This +/// is the same shape used by BearerScopeTests: real +/// subclass, real +/// with an injected handler, real +/// / +/// talking to it. +/// +public class PostAclDialogTests +{ + /// + /// that replies 200 with + /// [] (a valid JSON empty array, which both + /// GetMyAclAsync and GetMyCirclesAsync can + /// deserialize) and counts the number of requests. + /// + private sealed class CountingHttpHandler : HttpMessageHandler + { + public int RequestCount { get; private set; } + + protected override Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) + { + RequestCount++; + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("[]", Encoding.UTF8, "application/json"), + }; + return Task.FromResult(response); + } + } + + /// + /// Subclass of that routes HTTP + /// traffic through a caller-supplied + /// . Same recipe as + /// BearerScopeTests.TestableYavscApiClient — we + /// override CallAsync{T} to talk to our own + /// and skip the OIDC refresh path, + /// because the load-on-attach bug has nothing to do with + /// token refresh. + /// + private sealed class TestableYavscApiClient : YavscApiClient + { + private readonly HttpClient _http; + + public TestableYavscApiClient( + Settings settings, + TokenStore store, + HttpMessageHandler handler) + : base(settings, store, oidc: null!) + { + _http = new HttpClient(handler, disposeHandler: false); + } + + public override Task CallAsync( + HttpMethod method, string path, object? body = null, + CancellationToken ct = default) + { + var absolute = new Uri(new Uri(Settings.BusinessApiUrl), path); + using var req = new HttpRequestMessage(method, absolute); + using var resp = _http.SendAsync(req, ct).GetAwaiter().GetResult(); + resp.EnsureSuccessStatusCode(); + using var stream = resp.Content.ReadAsStream(); + var dto = JsonSerializer.Deserialize(stream, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + return Task.FromResult(dto!); + } + } + + /// + /// Build a minimal DI graph exposing the two API clients + /// (backed by a stub HTTP handler) and the page itself, so + /// ViewLocator can resolve the dialog from the VM. + /// Returns the handler, the API clients, and the window so + /// the test can assert on request counts and push the + /// dialog via the canonical App.PushPageAsync path. + /// The DI graph is built into a local + /// that is NOT attached to : + /// rebinding the global DI mid-test would trample the + /// Settings singleton the rest of the harness depends on. + /// + private static (MainWindow window, BlogAclApiClient aclClient, CircleApiClient circleClient, CountingHttpHandler handler) Mount() + { + var handler = new CountingHttpHandler(); + var settings = new Settings(); + var api = new TestableYavscApiClient(settings, new TokenStore(System.IO.Path.GetTempFileName()), handler); + var aclClient = new BlogAclApiClient(api, settings.BusinessApiUrl); + var circleClient = new CircleApiClient(api, settings.BusinessApiUrl); + + var services = new ServiceCollection(); + services.AddSingleton(settings); + services.AddSingleton(api); + services.AddSingleton(aclClient); + services.AddSingleton(circleClient); + services.AddTransient(); + var sp = services.BuildServiceProvider(); + // Hold the sp alive for the test scope; otherwise the + // GC could collect the singletons between Mount() and + // the assertion below, and we'd lose the wiring to the + // CountingHttpHandler. + GC.KeepAlive(sp); + + var window = new MainWindow(); + var app = (App)Application.Current!; + app.DataTemplates.Clear(); + app.DataTemplates.Add(new ViewLocator(sp)); + app.AttachMainWindow(window); + window.Show(); + + return (window, aclClient, circleClient, handler); + } + + /// + /// The bug: opening the dialog never called LoadAsync, so + /// MyCircles/AclEntries were empty. After the fix, setting + /// the dialog's DataContext to a PostAclDialogViewModel + /// (the same path App.PushPageAsync takes) must trigger + /// exactly one LoadAsync round-trip (the parallel WhenAll + /// inside the VM counts as one request per backend call, + /// hence two HTTP requests total: GET /blogacl and GET + /// /circle). + /// + [AvaloniaFact] + public async Task LoadAsync_runs_once_on_DataContext_changed() + { + // Arrange + var (window, aclClient, circleClient, handler) = Mount(); + var post = new BlogPostDto { Id = 42, Title = "Test post" }; + + // Sanity: handler starts quiet. + Assert.Equal(0, handler.RequestCount); + + // Act: push the dialog via the canonical VM-first pipeline. + // The locator goes through the parameterless ctor of + // PostAclDialog, then App.PushPageAsync assigns DataContext, + // which our hook intercepts to trigger LoadAsync. + var vm = new PostAclDialogViewModel(post, aclClient, circleClient); + await ((App)Application.Current!).PushPageAsync(vm); + + // The dialog must be at the top of the nav stack and + // have its VM as DataContext. + var dialog = window.NavRoot.NavigationStack[^1] as PostAclDialog + ?? throw new InvalidOperationException("Dialog not at top of stack"); + Assert.Same(vm, dialog.DataContext); + + // Drain pending async work. LoadAsync is async and the + // DataContextChanged handler is fire-and-forget; a + // couple of loop turns is enough. We poll the handler + // counter because the dispatch back onto the headless + // dispatcher isn't strict — using a generous-but-bounded + // wait avoids test flakes. + var deadline = DateTime.UtcNow.AddSeconds(2); + while (handler.RequestCount < 2 && DateTime.UtcNow < deadline) + { + await Task.Delay(20); + } + + // Assert: exactly two GETs went out (one to /blogacl, + // one to /circle), both from the LoadAsync call. + Assert.Equal(2, handler.RequestCount); + + // And the VM's idempotency gate has flipped. + Assert.True(vm.Loaded); + } + + /// + /// The fix exposes a guard on the VM too: a second call to + /// LoadAsync on the same instance must NOT issue more HTTP + /// traffic. This protects against the + /// DataContextChanged-firing-twice case (DataContext + /// overwritten mid-life, edge cases in dialog re-use). + /// + [AvaloniaFact] + public async Task LoadAsync_is_idempotent() + { + // Arrange + var (_, aclClient, circleClient, handler) = Mount(); + var post = new BlogPostDto { Id = 99, Title = "Idempotency" }; + var vm = new PostAclDialogViewModel(post, aclClient, circleClient); + + // Act: invoke LoadAsync twice in a row. + await vm.LoadAsync(); + await vm.LoadAsync(); + + // Assert: the second call short-circuited on _loaded. + Assert.Equal(2, handler.RequestCount); + Assert.True(vm.Loaded); + } +} diff --git a/src/PostIt.Tests/TestAppContext.cs b/src/PostIt.Tests/TestAppContext.cs new file mode 100644 index 00000000..2843c965 --- /dev/null +++ b/src/PostIt.Tests/TestAppContext.cs @@ -0,0 +1,11 @@ +using PostIt.Views; + +namespace PostIt.Tests; + +internal class TestAppContext +{ + public MainWindow? Window {get; set; } + public CirclesPage? page {get; set; } + public AddCircleMemberDialog? dialog { get; set; } + public App? App { get; internal set; } +} diff --git a/src/PostIt.Tests/pslist b/src/PostIt.Tests/pslist new file mode 100644 index 00000000..0f1d73da --- /dev/null +++ b/src/PostIt.Tests/pslist @@ -0,0 +1,94 @@ +UID PID PPID C STIME TTY TIME CMD +paul 1155 1 0 13:18 ? 00:00:00 /usr/lib/systemd/systemd --user +paul 1168 1155 0 13:18 ? 00:00:00 (sd-pam) +paul 1361 1155 0 13:18 ? 00:00:00 /usr/bin/dbus-daemon --session --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only +paul 1364 1155 1 13:18 ? 00:01:19 /home/paul/.nvm/versions/node/v22.23.0/bin/node /home/paul/.nvm/versions/node/v22.23.0/lib/node_modules/openclaw/dist/index.js gateway --port 18789 +paul 1367 1155 0 13:18 ? 00:00:00 /usr/bin/pipewire +paul 1372 1155 0 13:18 ? 00:00:00 /usr/bin/pipewire -c filter-chain.conf +paul 1373 1155 0 13:18 ? 00:00:00 /usr/bin/wireplumber +paul 1374 1155 0 13:18 ? 00:00:00 /usr/bin/pipewire-pulse +paul 1444 1155 0 13:18 ? 00:00:00 /usr/bin/mpris-proxy +paul 2593 1155 0 13:19 ? 00:00:00 /usr/bin/gnome-keyring-daemon --foreground --components=pkcs11,secrets --control-directory=/run/user/1000/keyring +paul 2608 2487 0 13:19 tty2 00:00:00 /usr/libexec/gdm-x-session --run-script /usr/bin/gnome-session +paul 2617 2608 1 13:19 tty2 00:01:12 /usr/lib/xorg/Xorg vt2 -displayfd 3 -auth /run/user/1000/gdm/Xauthority -nolisten tcp -background none -noreset -keeptty -novtswitch -verbose 3 +paul 2647 2608 0 13:19 tty2 00:00:00 /usr/libexec/gnome-session-binary +paul 2785 1155 0 13:19 ? 00:00:00 /usr/libexec/at-spi-bus-launcher +paul 2792 2785 0 13:19 ? 00:00:00 /usr/bin/dbus-daemon --config-file=/usr/share/defaults/at-spi2/accessibility.conf --nofork --print-address 11 --address=unix:path=/run/user/1000/at-spi/bus_1 +paul 2802 1155 0 13:19 ? 00:00:00 /usr/libexec/gcr-ssh-agent --base-dir /run/user/1000/gcr +paul 2803 1155 0 13:19 ? 00:00:00 /usr/libexec/gnome-session-ctl --monitor +paul 2804 1155 0 13:19 ? 00:00:00 /usr/bin/ssh-agent -D +paul 2814 1155 0 13:19 ? 00:00:00 /usr/libexec/gvfsd +paul 2828 1155 0 13:19 ? 00:00:00 /usr/libexec/gvfsd-fuse /run/user/1000/gvfs -f +paul 2838 1155 0 13:19 ? 00:00:00 /usr/libexec/gnome-session-binary --systemd-service --session=gnome +paul 2874 1155 3 13:19 ? 00:02:13 /usr/bin/gnome-shell +paul 2896 2874 0 13:19 ? 00:00:01 /usr/libexec/mutter-x11-frames +paul 2902 1155 0 13:19 ? 00:00:00 /usr/libexec/at-spi2-registryd --use-gnome-session +paul 2918 1155 0 13:19 ? 00:00:00 /usr/libexec/xdg-desktop-portal +paul 2933 1155 0 13:19 ? 00:00:00 /usr/libexec/xdg-permission-store +paul 2938 1155 0 13:19 ? 00:00:00 /usr/libexec/xdg-document-portal +paul 2971 1155 0 13:19 ? 00:00:00 /usr/libexec/gnome-shell-calendar-server +paul 2976 1155 0 13:19 ? 00:00:00 /usr/libexec/dconf-service +paul 2992 1155 0 13:19 ? 00:00:00 /usr/libexec/evolution-source-registry +paul 2994 1155 0 13:19 ? 00:00:00 /usr/bin/gjs -m /usr/share/gnome-shell/org.gnome.Shell.Notifications +paul 3012 1155 0 13:19 ? 00:00:12 /usr/bin/ibus-daemon --panel disable --xim +paul 3013 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-a11y-settings +paul 3014 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-color +paul 3015 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-datetime +paul 3016 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-housekeeping +paul 3018 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-keyboard +paul 3024 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-media-keys +paul 3025 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-power +paul 3027 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-print-notifications +paul 3029 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-rfkill +paul 3030 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-screensaver-proxy +paul 3035 2838 0 13:19 ? 00:00:05 /usr/bin/gnome-software --gapplication-service +paul 3037 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-sharing +paul 3042 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-smartcard +paul 3048 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-sound +paul 3054 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-usb-protection +paul 3057 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-wacom +paul 3058 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-xsettings +paul 3059 2838 0 13:19 ? 00:00:00 /usr/libexec/evolution-data-server/evolution-alarm-notify +paul 3064 2838 0 13:19 ? 00:00:00 /usr/bin/kalendarac +paul 3070 2838 0 13:19 ? 00:00:00 /usr/libexec/gsd-disk-utility-notify +paul 3088 2838 0 13:19 ? 00:00:00 /usr/bin/kdeconnectd +paul 3168 1155 0 13:19 ? 00:00:00 /usr/bin/gjs -m /usr/share/gnome-shell/org.gnome.ScreenSaver +paul 3172 1155 0 13:19 ? 00:00:00 /usr/libexec/gsd-printer +paul 3207 3012 0 13:19 ? 00:00:00 /usr/libexec/ibus-memconf +paul 3208 3012 0 13:19 ? 00:00:06 /usr/libexec/ibus-extension-gtk3 +paul 3214 1155 0 13:19 ? 00:00:00 /usr/libexec/ibus-x11 --kill-daemon +paul 3216 1155 0 13:19 ? 00:00:00 /usr/libexec/ibus-portal +paul 3218 1155 0 13:19 ? 00:00:00 /usr/libexec/localsearch-3 +paul 3219 1155 0 13:19 ? 00:00:00 /usr/libexec/xdg-desktop-portal-gnome +paul 3241 1155 0 13:19 ? 00:00:00 /usr/libexec/gvfs-udisks2-volume-monitor +paul 3251 1155 0 13:19 ? 00:00:00 /usr/libexec/gvfs-mtp-volume-monitor +paul 3259 1155 0 13:19 ? 00:00:00 /usr/libexec/gvfs-gphoto2-volume-monitor +paul 3265 1155 0 13:20 ? 00:00:00 /usr/libexec/gvfs-goa-volume-monitor +paul 3271 1155 0 13:20 ? 00:00:00 /usr/libexec/goa-daemon +paul 3280 1155 0 13:20 ? 00:00:00 /usr/libexec/goa-identity-service +paul 3287 1155 0 13:20 ? 00:00:00 /usr/libexec/gvfs-afc-volume-monitor +paul 3303 3012 0 13:20 ? 00:00:02 /usr/libexec/ibus-engine-simple +paul 3372 1155 0 13:20 ? 00:00:00 /usr/libexec/xdg-desktop-portal-gtk +paul 3441 1155 0 13:20 ? 00:00:00 /usr/libexec/gvfsd-metadata +paul 3453 1155 0 13:20 ? 00:00:00 /usr/libexec/evolution-calendar-factory +paul 3495 1155 0 13:20 ? 00:00:00 /usr/libexec/evolution-addressbook-factory +paul 4798 1155 0 13:26 ? 00:00:09 /usr/libexec/gnome-terminal-server +paul 4810 4798 0 13:26 pts/0 00:00:00 bash +paul 8614 1155 0 13:29 ? 00:00:01 /usr/bin/speech-dispatcher -s -t 0 +paul 8656 8614 0 13:29 ? 00:00:00 [sd_espeak-ng-mb] +paul 8709 8614 0 13:29 ? 00:00:00 /usr/lib/speech-dispatcher-modules/sd_espeak-ng /etc/speech-dispatcher/modules/espeak-ng.conf +paul 8785 8614 0 13:29 ? 00:00:00 /usr/lib/speech-dispatcher-modules/sd_dummy /etc/speech-dispatcher/modules/dummy.conf +paul 8799 8614 0 13:29 ? 00:00:00 /usr/lib/speech-dispatcher-modules/sd_espeak-ng /etc/speech-dispatcher/modules/ +paul 10028 1155 0 13:31 ? 00:00:00 adb -L tcp:5037 fork-server server --reply-fd 4 +paul 69578 2814 0 13:53 ? 00:00:00 /usr/libexec/gvfsd-http --spawner :1.22 /org/gtk/gvfs/exec_spaw/0 +paul 108341 1155 3 14:06 ? 00:00:48 /home/paul/.nvm/versions/node/v22.23.0/bin/node /home/paul/.nvm/versions/node/v22.23.0/lib/node_modules/acpx/dist/cli.js __queue-owner +paul 108416 108341 0 14:06 ? 00:00:00 openclaw +paul 108458 108416 2 14:06 ? 00:00:37 openclaw-acp +paul 143553 1155 0 14:19 ? 00:00:05 /home/paul/Workspace/yavsc/src/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests @@ /tmp/tmpI2JxLw.tmp +paul 149205 1155 0 14:21 ? 00:00:05 /home/paul/Workspace/yavsc/src/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests @@ /tmp/tmpitRyQG.tmp +paul 151724 1155 0 14:22 ? 00:00:04 /home/paul/Workspace/yavsc/src/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests @@ /tmp/tmpJEsOZV.tmp +paul 157447 1155 1 14:24 ? 00:00:05 /home/paul/Workspace/yavsc/src/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests @@ /tmp/tmpyM92DV.tmp +paul 165231 1155 0 14:26 ? 00:00:01 /home/paul/Workspace/yavsc/src/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests @@ /tmp/tmp5CKC19.tmp +paul 168472 1155 4 14:27 ? 00:00:09 /home/paul/Workspace/yavsc/src/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests @@ /tmp/tmpuRJsnQ.tmp +paul 172147 1155 4 14:29 pts/0 00:00:05 /home/paul/Workspace/yavsc/src/PostIt.Tests/bin/Debug/net10.0/PostIt.Tests @@ /tmp/tmpxT8nje.tmp +paul 172435 4810 99 14:31 pts/0 00:00:00 ps -fu paul diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index 1a68f4ac..d2399873 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -49,7 +49,7 @@ public partial class App : Application // build is ever reconfigured to skip the early check. if (TryHandOffCustomSchemeUrl()) return; - this.ServiceProvider = BuildServices(); + this.ServiceProvider = BuildServices(new ServiceCollection()); AttachServiceProvider(ServiceProvider); var settings = ServiceProvider.GetRequiredService(); var sessionStatus = ServiceProvider.GetRequiredService(); @@ -139,7 +139,7 @@ public partial class App : Application /// or service resolves through the same wiring the real app /// does, and a green test is a green contract for prod. /// - internal static IServiceProvider BuildServices() + internal static IServiceProvider BuildServices(ServiceCollection services) { var settings = new Settings(); settings.Load(); @@ -156,7 +156,6 @@ public partial class App : Application var contactService = new ContactService(); var userDirectory = new UserDirectory(userSearchClient); - var services = new ServiceCollection(); // Vues services.AddTransient(); @@ -350,4 +349,9 @@ public partial class App : Application return window.NavRoot.PushAsync(page); } + + internal async Task GoBackAsync() + { + await window.NavRoot.PopAsync(); + } } diff --git a/src/PostIt/PostIt/ViewModels/AddCircleMemberDialogViewModel.cs b/src/PostIt/PostIt/ViewModels/AddCircleMemberDialogViewModel.cs index a721d738..59d6dbed 100644 --- a/src/PostIt/PostIt/ViewModels/AddCircleMemberDialogViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/AddCircleMemberDialogViewModel.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using PostIt.Services; +using PostIt.Views; using Yavsc.Api.Client; namespace PostIt.ViewModels; @@ -106,7 +107,7 @@ public partial class AddCircleMemberDialogViewModel : ViewModelBase /// UI from firing an event with a null payload. /// [RelayCommand] - public void Add() + public async Task AddAsync() { if (Selected is null) { @@ -114,5 +115,14 @@ public partial class AddCircleMemberDialogViewModel : ViewModelBase return; } Confirmed?.Invoke(this, Selected); + var app = App.Current as App; + await app.GoBackAsync(); + } + + [RelayCommand] + public async Task CloseAsync() + { + var app = App.Current as App; + await app.GoBackAsync(); } } diff --git a/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs index bfc431b8..33a5bd30 100644 --- a/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs @@ -119,6 +119,17 @@ public partial class CirclesPageViewModel : ViewModelBase var directory = services.GetRequiredService(); AddCircleMemberDialogViewModel model = new AddCircleMemberDialogViewModel(directory); + // Wire the dialog's Confirmed event to OnAddMemberConfirmedAsync. + // Without this, the dialog's "Ajouter" button fires the event + // into the void: no subscriber, the picked user is silently + // dropped, and nothing is added to the circle. The dialog + // stays open until the user uses the back gesture — which is + // how the user noticed the button was a no-op. + // Async-void is intentional here: Confirmed is an + // EventHandler (returns void), and bridging to the + // async Task OnAddMemberConfirmedAsync requires it. + model.Confirmed += async (_, picked) => + await OnAddMemberConfirmedAsync(_, picked); await app.PushPageAsync(model); } /// diff --git a/src/PostIt/PostIt/ViewModels/PostAclDialogViewModel.cs b/src/PostIt/PostIt/ViewModels/PostAclDialogViewModel.cs index ae48fd8c..ae9e71d5 100644 --- a/src/PostIt/PostIt/ViewModels/PostAclDialogViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/PostAclDialogViewModel.cs @@ -1,14 +1,13 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Yavsc.Blogspot; using Yavsc.Api.Client; using Yavsc.Api.Client.Dtos; -using Yavsc.Abstract.Identity.Security; +using Yavsc.Abstract.BlogSpot; namespace PostIt.ViewModels; @@ -38,10 +37,12 @@ public partial class PostAclDialogViewModel : ViewModelBase public BlogPostDto Post { get; } [ObservableProperty] - public partial ObservableCollection MyCircles { get; set; } = new(); + public partial ObservableCollection + MyCircles { get; set; } = new(); [ObservableProperty] - public partial ObservableCollection AclEntries { get; set; } = new(); + public partial ObservableCollection + AclEntries { get; set; } = new(); [ObservableProperty] public partial CircleDto? SelectedCircleToAdd { get; set; } @@ -52,6 +53,22 @@ public partial class PostAclDialogViewModel : ViewModelBase [ObservableProperty] public partial string StatusMessage { get; set; } = string.Empty; + /// + /// Idempotency gate for : the dialog + /// attaches the load trigger in DataContextChanged, + /// which can fire more than once if the page is detached + /// and re-attached (dialog re-use, navigation edge cases) + /// with a different VM. Without this guard, the second load + /// would race against the first and could overwrite + /// mid-edit. Pattern copied from + /// Settings.Load. + /// + private bool _loaded; + + /// True once has run at least + /// once. Exposed for tests; do not bind from XAML. + public bool Loaded => _loaded; + public PostAclDialogViewModel( BlogPostDto post, BlogAclApiClient aclClient, @@ -68,6 +85,8 @@ public partial class PostAclDialogViewModel : ViewModelBase [RelayCommand] public async Task LoadAsync() { + if (_loaded) return; + IsBusy = true; try { @@ -83,6 +102,7 @@ public partial class PostAclDialogViewModel : ViewModelBase StatusMessage = $"{AclEntries.Count} autorisation(s)"; + _loaded = true; } catch (Exception ex) { @@ -106,9 +126,10 @@ public partial class PostAclDialogViewModel : ViewModelBase IsBusy = true; try { - var created = await _aclClient.GrantAsync(new CircleAuthorization + var created = await _aclClient.GrantAsync(new Yavsc.Abstract.BlogSpot.PostAccessControlRulePayload { - CircleId = SelectedCircleToAdd.Id + CircleId = SelectedCircleToAdd.Id, + BlogPostId = Post.Id }); if (created is not null) { @@ -131,7 +152,7 @@ public partial class PostAclDialogViewModel : ViewModelBase } [RelayCommand] - public async Task RevokeAsync(CircleAuthorization? acl) + public async Task RevokeAsync(PostAccessControlRulePayload? acl) { if (acl is null) return; IsBusy = true; diff --git a/src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml b/src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml index 5d1e2531..8b232988 100644 --- a/src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml +++ b/src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml @@ -6,6 +6,7 @@ xmlns:services="using:PostIt.Services" x:DataType="vm:AddCircleMemberDialogViewModel" > + @@ -29,7 +30,9 @@ + SelectedItem="{Binding Selected, Mode=TwoWay}" + MinHeight="20" + > @@ -47,11 +50,13 @@ /// /// Le path retourné est aligné sur - /// (minuscule). + /// (minuscule). /// Les anciens display templates utilisaient "/Avatars/" /// avec un S majuscule, en désaccord avec le path statique /// servi par le middleware de fichiers — les images ne @@ -29,8 +29,8 @@ namespace Yavsc.Abstract.Identity public static string AvatarSrc(IApplicationUser? user) { if (user==null || string.IsNullOrWhiteSpace(user?.UserName)) - return YavscConstants.DefaultAvatar; - return $"{YavscConstants.AvatarsPath}/{user!.UserName}.s.png"; + return Constants.DefaultAvatar; + return $"{Constants.AvatarsPath}/{user!.UserName}.s.png"; } } } diff --git a/src/Yavsc.Api.Client/BlogAclApiClient.cs b/src/Yavsc.Api.Client/BlogAclApiClient.cs index e8c8bf01..9a93d310 100644 --- a/src/Yavsc.Api.Client/BlogAclApiClient.cs +++ b/src/Yavsc.Api.Client/BlogAclApiClient.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Yavsc.Abstract.BlogSpot; using Yavsc.Abstract.Identity.Security; using Yavsc.Api.Client.Dtos; @@ -33,16 +34,16 @@ public sealed class BlogAclApiClient api.Http.BaseAddress = new Uri(blogsBaseAddress); } - public Task> GetMyAclAsync(CancellationToken ct = default) - => _api.CallAsync>(HttpMethod.Get, Path, ct: ct); + public Task> GetMyAclAsync(CancellationToken ct = default) + => _api.CallAsync>(HttpMethod.Get, Path, ct: ct); - public Task GetAclAsync(long circleId, CancellationToken ct = default) - => _api.CallAsync(HttpMethod.Get, $"{Path}/{circleId}", ct: ct); + public Task GetAclAsync(long circleId, CancellationToken ct = default) + => _api.CallAsync(HttpMethod.Get, $"{Path}/{circleId}", ct: ct); - public Task GrantAsync(CircleAuthorization acl, CancellationToken ct = default) - => _api.CallAsync(HttpMethod.Post, Path, body: acl, ct: ct); + public Task GrantAsync(PostAccessControlRulePayload acl, CancellationToken ct = default) + => _api.CallAsync(HttpMethod.Post, Path, body: acl, ct: ct); - public Task UpdateAclAsync(long circleId, CircleAuthorization acl, CancellationToken ct = default) + public Task UpdateAclAsync(long circleId, PostAccessControlRulePayload acl, CancellationToken ct = default) => _api.CallAsync(HttpMethod.Put, $"{Path}/{circleId}", body: acl, ct: ct); public Task RevokeAsync(long circleId, CancellationToken ct = default) diff --git a/src/Yavsc.Api/Controllers/Business/ActivityApiController.cs b/src/Yavsc.Api/Controllers/Business/ActivityApiController.cs index d2da2ea7..5aeddc57 100644 --- a/src/Yavsc.Api/Controllers/Business/ActivityApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/ActivityApiController.cs @@ -14,7 +14,7 @@ using Yavsc.Models.Workflow; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/activity")] + [Route(Constants.APIPrefix + "/activity")] public class ActivityApiController : Controller { private ApplicationDbContext _context; diff --git a/src/Yavsc.Api/Controllers/Business/BillingController.cs b/src/Yavsc.Api/Controllers/Business/BillingController.cs index 87035406..72180fc1 100644 --- a/src/Yavsc.Api/Controllers/Business/BillingController.cs +++ b/src/Yavsc.Api/Controllers/Business/BillingController.cs @@ -19,7 +19,7 @@ namespace Yavsc.ApiControllers using Yavsc.ViewModels.Auth; using Yavsc.Server.Helpers; - [Route("api/bill"), Authorize] + [Route(Constants.APIPrefix + "/bill"), Authorize] public class BillingController : Controller { readonly ApplicationDbContext dbContext; diff --git a/src/Yavsc.Api/Controllers/Business/BookQueryApiController.cs b/src/Yavsc.Api/Controllers/Business/BookQueryApiController.cs index 494075c6..7e58b071 100644 --- a/src/Yavsc.Api/Controllers/Business/BookQueryApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/BookQueryApiController.cs @@ -18,7 +18,7 @@ namespace Yavsc.Controllers using Yavsc.Server.Helpers; [Produces("application/json")] - [Route("api/bookquery"), Authorize("Performer")] + [Route(Constants.APIPrefix + "/bookquery"), Authorize("Performer")] public class BookQueryApiController : Controller { private ApplicationDbContext _context; diff --git a/src/Yavsc.Api/Controllers/Business/EstimateApiController.cs b/src/Yavsc.Api/Controllers/Business/EstimateApiController.cs index 41bdd353..902cb038 100644 --- a/src/Yavsc.Api/Controllers/Business/EstimateApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/EstimateApiController.cs @@ -15,7 +15,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/estimate"), Authorize] + [Route(Constants.APIPrefix + "/estimate"), Authorize] public class EstimateApiController : Controller { private readonly ApplicationDbContext _context; @@ -27,12 +27,12 @@ namespace Yavsc.Controllers } bool UserIsAdminOrThis(string uid) { - if (User.IsInRole(YavscConstants.AdminGroupName)) return true; + if (User.IsInRole(Constants.AdminGroupName)) return true; return uid == User.GetUserId(); } bool UserIsAdminOrInThese(string oid, string uid) { - if (User.IsInRole(YavscConstants.AdminGroupName)) return true; + if (User.IsInRole(Constants.AdminGroupName)) return true; var cuid = User.GetUserId(); return cuid == uid || cuid == oid; } @@ -82,7 +82,7 @@ namespace Yavsc.Controllers return BadRequest(); } var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (!User.IsInRole(YavscConstants.AdminGroupName)) + if (!User.IsInRole(Constants.AdminGroupName)) { if (uid != estimate.OwnerId) { @@ -118,7 +118,7 @@ namespace Yavsc.Controllers var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); if (estimate.OwnerId == null) estimate.OwnerId = uid; - if (!User.IsInRole(YavscConstants.AdminGroupName)) + if (!User.IsInRole(Constants.AdminGroupName)) { if (uid != estimate.OwnerId) { @@ -187,7 +187,7 @@ namespace Yavsc.Controllers return NotFound(); } var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (!User.IsInRole(YavscConstants.AdminGroupName)) + if (!User.IsInRole(Constants.AdminGroupName)) { if (uid != estimate.OwnerId) { diff --git a/src/Yavsc.Api/Controllers/Business/EstimateTemplatesApiController.cs b/src/Yavsc.Api/Controllers/Business/EstimateTemplatesApiController.cs index 4442e0b3..81de4cac 100644 --- a/src/Yavsc.Api/Controllers/Business/EstimateTemplatesApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/EstimateTemplatesApiController.cs @@ -9,7 +9,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/EstimateTemplatesApi")] + [Route(Constants.APIPrefix + "/EstimateTemplatesApi")] public class EstimateTemplatesApiController : Controller { private ApplicationDbContext _context; @@ -62,7 +62,7 @@ namespace Yavsc.Controllers } var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); if (estimateTemplate.OwnerId!=uid) - if (!User.IsInRole(YavscConstants.AdminGroupName)) + if (!User.IsInRole(Constants.AdminGroupName)) return new StatusCodeResult(StatusCodes.Status403Forbidden); _context.Entry(estimateTemplate).State = EntityState.Modified; @@ -132,7 +132,7 @@ namespace Yavsc.Controllers } var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); if (estimateTemplate.OwnerId!=uid) - if (!User.IsInRole(YavscConstants.AdminGroupName)) + if (!User.IsInRole(Constants.AdminGroupName)) return new StatusCodeResult(StatusCodes.Status403Forbidden); _context.EstimateTemplates.Remove(estimateTemplate); diff --git a/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs b/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs index b91cba51..c05da827 100644 --- a/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs @@ -8,7 +8,7 @@ using Yavsc.ViewModels.FrontOffice; namespace Yavsc.ApiControllers { - [Route("api/front")] + [Route(Constants.APIPrefix + "/front")] public class FrontOfficeApiController : Controller { ApplicationDbContext dbContext; diff --git a/src/Yavsc.Api/Controllers/Business/PaymentApiController.cs b/src/Yavsc.Api/Controllers/Business/PaymentApiController.cs index 3076dbe1..5f769e4e 100644 --- a/src/Yavsc.Api/Controllers/Business/PaymentApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/PaymentApiController.cs @@ -6,7 +6,7 @@ using Yavsc.Models; namespace Yavsc.ApiControllers { - [Route("api/payment")] + [Route(Constants.APIPrefix + "/payment")] public class PaymentApiController : Controller { private readonly ApplicationDbContext dbContext; diff --git a/src/Yavsc.Api/Controllers/Business/PerformersApiController.cs b/src/Yavsc.Api/Controllers/Business/PerformersApiController.cs index b552eff3..2ad1ded5 100644 --- a/src/Yavsc.Api/Controllers/Business/PerformersApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/PerformersApiController.cs @@ -11,7 +11,7 @@ namespace Yavsc.Controllers using Yavsc.Services; [Produces("application/json")] - [Route("api/performers")] + [Route(Constants.APIPrefix + "/performers")] public class PerformersApiController : Controller { ApplicationDbContext dbContext; diff --git a/src/Yavsc.Api/Controllers/Business/ProductApiController.cs b/src/Yavsc.Api/Controllers/Business/ProductApiController.cs index abd621c3..97a60fdb 100644 --- a/src/Yavsc.Api/Controllers/Business/ProductApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/ProductApiController.cs @@ -9,7 +9,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/ProductApi")] + [Route(Constants.APIPrefix + "/ProductApi")] public class ProductApiController : Controller { private readonly ApplicationDbContext _context; @@ -46,7 +46,7 @@ namespace Yavsc.Controllers } // PUT: api/ProductApi/5 - [HttpPut("{id}"),Authorize(YavscConstants.FrontOfficeGroupName)] + [HttpPut("{id}"),Authorize(Constants.FrontOfficeGroupName)] public IActionResult PutProduct(long id, [FromBody] Product product) { if (!ModelState.IsValid) @@ -81,7 +81,7 @@ namespace Yavsc.Controllers } // POST: api/ProductApi - [HttpPost,Authorize(YavscConstants.FrontOfficeGroupName)] + [HttpPost,Authorize(Constants.FrontOfficeGroupName)] public IActionResult PostProduct([FromBody] Product product) { if (!ModelState.IsValid) @@ -110,7 +110,7 @@ namespace Yavsc.Controllers } // DELETE: api/ProductApi/5 - [HttpDelete("{id}"),Authorize(YavscConstants.FrontOfficeGroupName)] + [HttpDelete("{id}"),Authorize(Constants.FrontOfficeGroupName)] public IActionResult DeleteProduct(long id) { if (!ModelState.IsValid) diff --git a/src/Yavsc.Api/Controllers/HairCut/BursherProfilesApiController.cs b/src/Yavsc.Api/Controllers/HairCut/BursherProfilesApiController.cs index 22fdf1e9..cd3a561b 100644 --- a/src/Yavsc.Api/Controllers/HairCut/BursherProfilesApiController.cs +++ b/src/Yavsc.Api/Controllers/HairCut/BursherProfilesApiController.cs @@ -8,7 +8,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/bursherprofiles")] + [Route(Constants.APIPrefix + "/bursherprofiles")] public class BursherProfilesApiController : Controller { private readonly ApplicationDbContext _context; @@ -57,7 +57,7 @@ namespace Yavsc.Controllers { return BadRequest(); } - + if (id != User.GetUserId()) { return BadRequest(); diff --git a/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs b/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs index 822c3182..c1181f54 100644 --- a/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs +++ b/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs @@ -24,7 +24,7 @@ namespace Yavsc.ApiControllers using Microsoft.AspNetCore.Authorization; using Yavsc.Server.Helpers; - [Route("api/haircut")][Authorize] + [Route(Constants.APIPrefix + "/haircut")][Authorize] public class HairCutController : Controller { private readonly ApplicationDbContext _context; diff --git a/src/Yavsc.Api/Controllers/HyperLinkApiController.cs b/src/Yavsc.Api/Controllers/HyperLinkApiController.cs index b2d28baa..3ba74219 100644 --- a/src/Yavsc.Api/Controllers/HyperLinkApiController.cs +++ b/src/Yavsc.Api/Controllers/HyperLinkApiController.cs @@ -6,7 +6,7 @@ using Yavsc.Models.Relationship; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/hyperlink")] + [Route(Constants.APIPrefix + "/hyperlink")] public class HyperLinkApiController : Controller { private ApplicationDbContext _context; diff --git a/src/Yavsc.Api/Controllers/IT/GitRefsApiController.cs b/src/Yavsc.Api/Controllers/IT/GitRefsApiController.cs index 55ae08b7..67f38d22 100644 --- a/src/Yavsc.Api/Controllers/IT/GitRefsApiController.cs +++ b/src/Yavsc.Api/Controllers/IT/GitRefsApiController.cs @@ -7,7 +7,7 @@ using Yavsc.Server.Models.IT.SourceCode; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/GitRefsApi")] + [Route(Constants.APIPrefix + "/GitRefsApi")] [Authorize("AdministratorOnly")] public class GitRefsApiController : Controller { diff --git a/src/Yavsc.Api/Controllers/MailTemplatingApiController.cs b/src/Yavsc.Api/Controllers/MailTemplatingApiController.cs index 958ade66..c289c3da 100644 --- a/src/Yavsc.Api/Controllers/MailTemplatingApiController.cs +++ b/src/Yavsc.Api/Controllers/MailTemplatingApiController.cs @@ -2,9 +2,9 @@ using Microsoft.AspNetCore.Mvc; namespace Yavsc.ApiControllers { - [Route("api/mailtemplate")] + [Route(Constants.APIPrefix + "/mailtemplate")] public class MailTemplatingApiController: Controller { - + } } diff --git a/src/Yavsc.Api/Controllers/MailingTemplateApiController.cs b/src/Yavsc.Api/Controllers/MailingTemplateApiController.cs index dc535476..4373d847 100644 --- a/src/Yavsc.Api/Controllers/MailingTemplateApiController.cs +++ b/src/Yavsc.Api/Controllers/MailingTemplateApiController.cs @@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/mailing")] + [Route(Constants.APIPrefix + "/mailing")] [Authorize("AdministratorOnly")] public class MailingTemplateApiController : Controller { diff --git a/src/Yavsc.Api/Controllers/Musical/MusicalPreferencesApiController.cs b/src/Yavsc.Api/Controllers/Musical/MusicalPreferencesApiController.cs index 944b335b..dc935c14 100644 --- a/src/Yavsc.Api/Controllers/Musical/MusicalPreferencesApiController.cs +++ b/src/Yavsc.Api/Controllers/Musical/MusicalPreferencesApiController.cs @@ -8,7 +8,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/museprefs")] + [Route(Constants.APIPrefix + "/museprefs")] public class MusicalPreferencesApiController : Controller { private readonly ApplicationDbContext _context; diff --git a/src/Yavsc.Api/Controllers/Musical/MusicalTendenciesApiController.cs b/src/Yavsc.Api/Controllers/Musical/MusicalTendenciesApiController.cs index eacccb0a..e72090f6 100644 --- a/src/Yavsc.Api/Controllers/Musical/MusicalTendenciesApiController.cs +++ b/src/Yavsc.Api/Controllers/Musical/MusicalTendenciesApiController.cs @@ -8,7 +8,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/MusicalTendenciesApi")] + [Route(Constants.APIPrefix + "/MusicalTendenciesApi")] public class MusicalTendenciesApiController : Controller { private readonly ApplicationDbContext _context; diff --git a/src/Yavsc.Api/Controllers/PostRateApiController.cs b/src/Yavsc.Api/Controllers/PostRateApiController.cs index dc132da4..50d6d2e9 100644 --- a/src/Yavsc.Api/Controllers/PostRateApiController.cs +++ b/src/Yavsc.Api/Controllers/PostRateApiController.cs @@ -37,7 +37,7 @@ namespace Yavsc.Controllers var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); if (blogpost.AuthorId!=uid) - if (!User.IsInRole(YavscConstants.AdminGroupName)) + if (!User.IsInRole(Constants.AdminGroupName)) return BadRequest(); _context.SaveChanges(User.GetUserId()); diff --git a/src/Yavsc.Api/Controllers/ProfileApiController.cs b/src/Yavsc.Api/Controllers/ProfileApiController.cs index 60ad1f60..93bf2a4e 100644 --- a/src/Yavsc.Api/Controllers/ProfileApiController.cs +++ b/src/Yavsc.Api/Controllers/ProfileApiController.cs @@ -7,8 +7,8 @@ namespace Yavsc.ApiControllers /// /// Base class for managing performers profiles /// - [Produces("application/json"),Route("api/profile")] - public abstract class ProfileApiController : Controller + [Produces("application/json"),Route(Constants.APIPrefix + "/profile")] + public abstract class ProfileApiController : Controller { public ProfileApiController() { } diff --git a/src/Yavsc.Api/Controllers/Relationship/BlackListApiController.cs b/src/Yavsc.Api/Controllers/Relationship/BlackListApiController.cs index ebc1c03b..32cf8495 100644 --- a/src/Yavsc.Api/Controllers/Relationship/BlackListApiController.cs +++ b/src/Yavsc.Api/Controllers/Relationship/BlackListApiController.cs @@ -10,7 +10,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/blacklist"), Authorize] + [Route(Constants.APIPrefix + "/blacklist"), Authorize] public class BlackListApiController : Controller { private readonly ApplicationDbContext _context; @@ -50,8 +50,8 @@ namespace Yavsc.Controllers { var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); if (uid != blackListed.OwnerId) - if (!User.IsInRole(YavscConstants.AdminGroupName)) - if (!User.IsInRole(YavscConstants.FrontOfficeGroupName)) + if (!User.IsInRole(Constants.AdminGroupName)) + if (!User.IsInRole(Constants.FrontOfficeGroupName)) return false; return true; } @@ -140,7 +140,7 @@ namespace Yavsc.Controllers if (!CheckPermission(blackListed)) return BadRequest(); - + _context.BlackListed.Remove(blackListed); _context.SaveChanges(User.GetUserId()); diff --git a/src/Yavsc.Api/Controllers/Relationship/ChatApiController.cs b/src/Yavsc.Api/Controllers/Relationship/ChatApiController.cs index cdaeecde..b991c0fb 100644 --- a/src/Yavsc.Api/Controllers/Relationship/ChatApiController.cs +++ b/src/Yavsc.Api/Controllers/Relationship/ChatApiController.cs @@ -9,14 +9,14 @@ using Microsoft.EntityFrameworkCore; namespace Yavsc.Controllers { - [Route("api/chat")] + [Route(Constants.APIPrefix + "/chat")] public class ChatApiController : Controller { readonly ApplicationDbContext dbContext; readonly UserManager userManager; private readonly IConnexionManager _cxManager; public ChatApiController(ApplicationDbContext dbContext, - UserManager userManager, + UserManager userManager, IConnexionManager cxManager) { this.dbContext = dbContext; diff --git a/src/Yavsc.Api/Controllers/Relationship/ChatRoomAccessApiController.cs b/src/Yavsc.Api/Controllers/Relationship/ChatRoomAccessApiController.cs index 5fe3a0bf..fba8bd43 100644 --- a/src/Yavsc.Api/Controllers/Relationship/ChatRoomAccessApiController.cs +++ b/src/Yavsc.Api/Controllers/Relationship/ChatRoomAccessApiController.cs @@ -9,7 +9,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/ChatRoomAccessApi")] + [Route(Constants.APIPrefix + "/ChatRoomAccessApi")] public class ChatRoomAccessApiController : Controller { private readonly ApplicationDbContext _context; @@ -37,7 +37,7 @@ namespace Yavsc.Controllers ChatRoomAccess chatRoomAccess = await _context.ChatRoomAccess.SingleAsync(m => m.ChannelName == id); - + if (chatRoomAccess == null) { @@ -46,13 +46,13 @@ namespace Yavsc.Controllers var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); if (uid != chatRoomAccess.UserId && uid != chatRoomAccess.Room.OwnerId - && ! User.IsInMsRole(YavscConstants.AdminGroupName)) - + && ! User.IsInMsRole(Constants.AdminGroupName)) + { ModelState.AddModelError("UserId","get refused"); return BadRequest(ModelState); } - + return Ok(chatRoomAccess); } @@ -72,7 +72,7 @@ namespace Yavsc.Controllers } var room = _context.ChatRoom.First(channel => channel.Name == chatRoomAccess.ChannelName ); - if (uid != room.OwnerId && ! User.IsInMsRole(YavscConstants.AdminGroupName)) + if (uid != room.OwnerId && ! User.IsInMsRole(Constants.AdminGroupName)) { ModelState.AddModelError("ChannelName", "access put refused"); return BadRequest(ModelState); @@ -110,7 +110,7 @@ namespace Yavsc.Controllers var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); var room = _context.ChatRoom.First(channel => channel.Name == chatRoomAccess.ChannelName ); - if (room == null || (uid != room.OwnerId && ! User.IsInMsRole(YavscConstants.AdminGroupName))) + if (room == null || (uid != room.OwnerId && ! User.IsInMsRole(Constants.AdminGroupName))) { ModelState.AddModelError("ChannelName", "access post refused"); return BadRequest(ModelState); @@ -154,7 +154,7 @@ namespace Yavsc.Controllers var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); var room = _context.ChatRoom.First(channel => channel.Name == chatRoomAccess.ChannelName ); - if (room == null || (uid != room.OwnerId && chatRoomAccess.UserId != uid && ! User.IsInMsRole(YavscConstants.AdminGroupName))) + if (room == null || (uid != room.OwnerId && chatRoomAccess.UserId != uid && ! User.IsInMsRole(Constants.AdminGroupName))) { ModelState.AddModelError("UserId", "access drop refused"); return BadRequest(ModelState); diff --git a/src/Yavsc.Api/Controllers/Relationship/ChatRoomApiController.cs b/src/Yavsc.Api/Controllers/Relationship/ChatRoomApiController.cs index 990646fc..5d59f6bd 100644 --- a/src/Yavsc.Api/Controllers/Relationship/ChatRoomApiController.cs +++ b/src/Yavsc.Api/Controllers/Relationship/ChatRoomApiController.cs @@ -8,7 +8,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/ChatRoomApi")] + [Route(Constants.APIPrefix + "/ChatRoomApi")] public class ChatRoomApiController : Controller { private readonly ApplicationDbContext _context; @@ -128,7 +128,7 @@ namespace Yavsc.Controllers } ChatRoom chatRoom = await _context.ChatRoom.SingleAsync(m => m.Name == id); - + if (chatRoom == null) { @@ -137,7 +137,7 @@ namespace Yavsc.Controllers if (User.GetUserId() != chatRoom.OwnerId ) { - if (!User.IsInMsRole(YavscConstants.AdminGroupName)) + if (!User.IsInMsRole(Constants.AdminGroupName)) return BadRequest(new {error = "OwnerId"}); } diff --git a/src/Yavsc.Api/Controllers/Relationship/ContactsApiController.cs b/src/Yavsc.Api/Controllers/Relationship/ContactsApiController.cs index ffd6eb0b..96ba03dc 100644 --- a/src/Yavsc.Api/Controllers/Relationship/ContactsApiController.cs +++ b/src/Yavsc.Api/Controllers/Relationship/ContactsApiController.cs @@ -8,7 +8,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/ContactsApi")] + [Route(Constants.APIPrefix + "/ContactsApi")] public class ContactsApiController : Controller { private readonly ApplicationDbContext _context; diff --git a/src/Yavsc.Api/Controllers/ServiceApiController.cs b/src/Yavsc.Api/Controllers/ServiceApiController.cs index e9330543..8556fb5a 100644 --- a/src/Yavsc.Api/Controllers/ServiceApiController.cs +++ b/src/Yavsc.Api/Controllers/ServiceApiController.cs @@ -9,7 +9,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/ServiceApi")] + [Route(Constants.APIPrefix + "/ServiceApi")] public class ServiceApiController : Controller { private readonly ApplicationDbContext _context; @@ -46,7 +46,7 @@ namespace Yavsc.Controllers } // PUT: api/ServiceApi/5 - [HttpPut("{id}"),Authorize(YavscConstants.FrontOfficeGroupName)] + [HttpPut("{id}"),Authorize(Constants.FrontOfficeGroupName)] public IActionResult PutService(long id, [FromBody] Service service) { if (!ModelState.IsValid) @@ -81,7 +81,7 @@ namespace Yavsc.Controllers } // POST: api/ServiceApi - [HttpPost,Authorize(YavscConstants.FrontOfficeGroupName)] + [HttpPost,Authorize(Constants.FrontOfficeGroupName)] public IActionResult PostService([FromBody] Service service) { if (!ModelState.IsValid) @@ -110,7 +110,7 @@ namespace Yavsc.Controllers } // DELETE: api/ServiceApi/5 - [HttpDelete("{id}"),Authorize(YavscConstants.FrontOfficeGroupName)] + [HttpDelete("{id}"),Authorize(Constants.FrontOfficeGroupName)] public IActionResult DeleteService(long id) { if (!ModelState.IsValid) diff --git a/src/Yavsc.Api/Controllers/accounting/ApplicationUserApiController.cs b/src/Yavsc.Api/Controllers/accounting/ApplicationUserApiController.cs index 11c70d60..cb565a0d 100644 --- a/src/Yavsc.Api/Controllers/accounting/ApplicationUserApiController.cs +++ b/src/Yavsc.Api/Controllers/accounting/ApplicationUserApiController.cs @@ -13,7 +13,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json"),Authorize("AdministratorOnly")] - [Route("api/users")] + [Route(Constants.APIPrefix + "/users")] public class ApplicationUserApiController : Controller { private readonly ApplicationDbContext _context; @@ -28,7 +28,7 @@ namespace Yavsc.Controllers public IEnumerable GetApplicationUser(int skip=0, int take = 25) { return _context.Users.Skip(skip).Take(take) - .Select(u=> new UserInfo{ + .Select(u=> new UserInfo{ UserId = u.Id, UserName = u.UserName, Avatar = u.Avatar}); @@ -39,7 +39,7 @@ namespace Yavsc.Controllers { return _context.Users.Where(u => u.UserName.Contains(pattern)) .Skip(skip).Take(take) - .Select(u=> new UserInfo { + .Select(u=> new UserInfo { UserId = u.Id, UserName = u.UserName, Avatar = u.Avatar }); diff --git a/src/Yavsc.Blogs.Tests/BlogAclApiTests.cs b/src/Yavsc.Blogs.Tests/BlogAclApiTests.cs new file mode 100644 index 00000000..49259d05 --- /dev/null +++ b/src/Yavsc.Blogs.Tests/BlogAclApiTests.cs @@ -0,0 +1,221 @@ +using System.Net; +using System.Net.Http.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Yavsc.Abstract.BlogSpot; +using Yavsc.Models; +using Yavsc.Models.Access; +using Yavsc.Models.Blog; +using Yavsc.Models.Relationship; +using Yavsc.Tests.Shared; +using static Yavsc.Constants; + +namespace Yavsc.Blogs.Tests; + +/// +/// Behavioural tests for BlogAclApiController.PostCircleAuthorizationToBlogPost: +/// POST /api/v1/blogacl with a JSON body of +/// CircleAuthorizationToBlogPost (CircleId + BlogPostId). +/// +/// Same fixture as : +/// provides a SQLite +/// :memory: ApplicationDbContext (so FKs are +/// enforced the way a real relational engine would) and JWT +/// bearer auth via TestTokenIssuer. No mocks — the real +/// DbContext receives the real INSERT attempt. +/// +/// The bug being pinned by these tests: the POST endpoint +/// calls _context.CircleAuthorizationToBlogPost.Add(...) +/// then SaveChangesAsync. The entity has a composite +/// key (CircleId + BlogPostId) and two FKs; EF Core refuses +/// the INSERT with +/// System.InvalidOperationException: The value of +/// 'CircleAuthorizationToBlogPost.BlogPostId' is unknown when +/// attempting to save changes when the principal entities +/// (the existing BlogPost and Circle) are not +/// attached to the DbContext in the same change-tracker graph. +/// +[Collection("Yavsc Blogs")] +public sealed class BlogAclApiTests : IClassFixture +{ + private readonly BlogsWebServerFixture _fixture; + + + public BlogAclApiTests(BlogsWebServerFixture fixture) + { + _fixture = fixture; + } + + + private string BlogAclUrl() + => $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{APIPrefix}/blogacl"; + + /// Delete any ACL rows tied to the fixture's seeded + /// (CircleId, BlogPostId) pair. The shared SQLite store + /// persists across tests, so tests that POST a successful ACL + /// row would otherwise conflict with whichever other test runs + /// next against the same pair — xUnit does not guarantee + /// execution order. Calling this at the start of each + /// insert-bearing test guarantees a clean slate regardless of + /// the previous test's outcome. + private void CleanupAcl() + { + using var scope = _fixture.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + db.CircleAuthorizationToBlogPost + .Where(a => a.CircleId == _fixture.CircleId + && a.BlogPostId == _fixture.PostId) + .ExecuteDelete(); + } + + private HttpClient NewClient(string subject) + { + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (_, _, _, _) => true + }; + var http = new HttpClient(handler) + { + BaseAddress = new Uri(_fixture.Addresses.First(a => a.StartsWith("https://"))) + }; + // The Blogs fixture disables JwtSecurityTokenHandler's + // inbound claim-type remap, so the JWT's "sub" stays "sub" + // rather than being rewritten to ClaimTypes.NameIdentifier. + // The controller, however, reads the user id via + // User.FindFirstValue(ClaimTypes.NameIdentifier), so we add + // an explicit nameid claim to keep the legacy lookup happy. + http.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue( + "Bearer", + TestTokenIssuer.Issue( + subject, + extraClaims: new[] + { + new System.Security.Claims.Claim( + System.Security.Claims.ClaimTypes.NameIdentifier, + subject), + })); + return http; + } + + /// + /// Reproduces the prod 500 logged on 2026-08-21 on mercure: + /// InvalidOperationException: The value of + /// 'CircleAuthorizationToBlogPost.BlogPostId' is unknown + /// when POSTs the + /// shape { "circleId": <id> } — the exact body the + /// PostIt client builds from + /// (which only carries CircleId). The server deserialises + /// it into , leaves + /// BlogPostId at its default(long) = 0, attaches + /// no Target navigation, and EF Core refuses to INSERT + /// during PrepareToSave(). The fix lives in PostIt + /// (enrich the payload with blogPostId + comment) + /// and on the wire DTO ( must + /// carry those fields); the server validates. Until that ships, + /// this test stays red. + /// + [Fact] + public async Task PostCircleAuthorization_returns_201_when_payload_mirrors_PostIt_shape_against_existing_circle_named_test() + { + // The prod circle already exists with Name="test", Public=true, + // owned by the caller. We seed the same shape pre-POST so the + // test reproduces the prod scenario end-to-end. + CleanupAcl(); + using var http = NewClient("alice"); + + var payload = new PostAccessControlRulePayload + { + CircleId = _fixture.CircleId, + BlogPostId = _fixture.PostId + }; + + var response = await http.PostAsJsonAsync(BlogAclUrl(), payload, + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + } + + /// + /// Payload templates for . + /// Each row carries the shape we want to POST; -1L and + /// -2L are negative sentinels that the test substitutes + /// with the ids of freshly seeded Circle / BlogPost + /// rows before sending, so every shape lands against a real + /// principal entity and the seeded fixtures are not dead. + /// + public static IEnumerable BlogAclPayloadsForNever500() + { + + // circleId only (the historical bug shape, 2026-08-21 mercure): + // must be rejected, never 500. + return new object[][] + { + [ + new PostAccessControlRulePayload + { + BlogPostId = -2, + CircleId = -1 + } + ], + [new PostAccessControlRulePayload + { + BlogPostId = 1, + CircleId = -1 + } + ], + [new PostAccessControlRulePayload + { + BlogPostId = 1, + CircleId = 1 + } + ] + } ; + } + + /// + /// Hard rule (Paul, 2026-08-21): a 500 is never acceptable + /// + [Theory] + [MemberData(nameof(BlogAclPayloadsForNever500))] + public async Task PostCircleAuthorization_never_returns_500(PostAccessControlRulePayload payload) + { + using var http = NewClient("alice"); + + var response = await http.PostAsJsonAsync( + BlogAclUrl(), payload, + TestContext.Current.CancellationToken); + + Assert.NotEqual(HttpStatusCode.InternalServerError, response.StatusCode); + } + + [Fact] + async Task PostCircleAuthorization_dosent_return_500 () + { + CleanupAcl(); + await PostCircleAuthorization_never_returns_500( + + new PostAccessControlRulePayload + { + BlogPostId = -1, + CircleId = _fixture.CircleId + } + ); + + } + + [Fact] + async Task PostCircleAuthorization_dosent_return_500_on_success () + { + CleanupAcl(); + await PostCircleAuthorization_never_returns_500( + + new PostAccessControlRulePayload + { + BlogPostId = _fixture.PostId, + CircleId = _fixture.CircleId + } + ); + + } +} diff --git a/src/Yavsc.Blogs.Tests/BlogApiSmokeTests.cs b/src/Yavsc.Blogs.Tests/BlogApiSmokeTests.cs index b84e75b6..162d76fe 100644 --- a/src/Yavsc.Blogs.Tests/BlogApiSmokeTests.cs +++ b/src/Yavsc.Blogs.Tests/BlogApiSmokeTests.cs @@ -12,6 +12,7 @@ namespace Yavsc.Blogs.Tests; /// surface. The first behavioural test (GET /api/v1/blog returns /// 200) lands in a follow-up commit. /// +[Collection("Yavsc Blogs")] public sealed class BlogApiSmokeTests : IClassFixture { private readonly BlogsWebServerFixture _fixture; diff --git a/src/Yavsc.Blogs.Tests/BlogApiTests.cs b/src/Yavsc.Blogs.Tests/BlogApiTests.cs index cc7aaec8..bde5a6cc 100644 --- a/src/Yavsc.Blogs.Tests/BlogApiTests.cs +++ b/src/Yavsc.Blogs.Tests/BlogApiTests.cs @@ -22,7 +22,7 @@ namespace Yavsc.Blogs.Tests; /// header (or sending a token signed with the wrong key) gets a /// 401 back from the framework. /// -[Collection("JwtClaimMapping")] +[Collection("Yavsc Blogs")] public sealed class BlogApiTests : IClassFixture { private readonly BlogsWebServerFixture _fixture; @@ -45,6 +45,21 @@ public sealed class BlogApiTests : IClassFixture db.Database.EnsureCreated(); } + /// Reset the database and seed the + /// tester row. Required + /// for any test that POST/PUT/DELETE a BlogPost: + /// BlogPost.AuthorId is a FK to + /// AspNetUsers.Id, and SQLite (unlike the EF Core + /// InMemory provider) enforces it. Without the seed, the + /// POST handler hits + /// SQLite Error 19: 'FOREIGN KEY constraint failed' + /// at SaveChanges and the controller returns 500. + private void ResetAndSeedDefaultUser() + { + ResetDatabase(); + _fixture.SeedUser("tester"); + } + /// The fixture's WebApplication is bound to /// https://localhost:<random> via /// . We pick the first @@ -116,7 +131,7 @@ public sealed class BlogApiTests : IClassFixture [Fact] public async Task PostBlog_creates_a_post_and_Get_returns_it_in_the_list() { - ResetDatabase(); + ResetAndSeedDefaultUser(); using var http = NewClient(); // Create a minimal BlogPost. The server assigns Id, so we @@ -154,7 +169,7 @@ public sealed class BlogApiTests : IClassFixture [Fact] public async Task PostBlog_sets_AuthorId_on_created_post_and_list_entry() { - ResetDatabase(); + ResetAndSeedDefaultUser(); using var http = NewClient(subject: "tester"); var draft = new BlogPost @@ -186,7 +201,7 @@ public sealed class BlogApiTests : IClassFixture [Fact] public async Task PostBlogComment_returns_201_for_existing_post() { - ResetDatabase(); + ResetAndSeedDefaultUser(); using var http = NewClient(subject: "tester"); var draft = new BlogPost @@ -249,7 +264,7 @@ public sealed class BlogApiTests : IClassFixture [Fact] public async Task PutBlog_with_valid_token_and_owner_returns_204_and_Get_reflects_update() { - ResetDatabase(); + ResetAndSeedDefaultUser(); // The JWT's sub must match the post's AuthorId: // PermissionHandler.IsOwner checks blog.AuthorId == user.GetUserId(), // and UserHelpers.GetUserId reads "sub" off the principal. @@ -300,7 +315,7 @@ public sealed class BlogApiTests : IClassFixture [Fact] public async Task DeleteBlog_removes_a_post_and_Get_returns_an_empty_list() { - ResetDatabase(); + ResetAndSeedDefaultUser(); using var http = NewClient(); // Seed a post we can delete. @@ -342,7 +357,7 @@ public sealed class BlogApiTests : IClassFixture // ModelState validation starts rejecting the PostIt payload // (missing field, wrong casing, etc.), this test fails // before the regression reaches a user. - ResetDatabase(); + ResetAndSeedDefaultUser(); using var http = NewClient(subject: "tester"); // Mirrors what MainPageViewModel.Save builds: a BlogPost with diff --git a/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs b/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs index 1e610082..218904ef 100644 --- a/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs +++ b/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs @@ -1,27 +1,33 @@ -using System.Text; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using Yavsc.Blogs.Controllers; using Yavsc.Models; +using Yavsc.Models.Blog; +using Yavsc.Models.Relationship; using Yavsc.Services; using Yavsc.Tests.Shared; namespace Yavsc.Blogs.Tests; /// -/// Test host for the Yavsc.Blogs API surface. Specialisation of -/// that wires up only the bits the -/// blog API actually depends on: +/// Shared integration-test host for the Yavsc.Blogs API surface. +/// Specialisation of that wires up +/// only the bits the blog API actually depends on: /// /// -/// An in-memory -/// (the real one — no mock) so BlogSpotService.Index can run -/// against an empty table and return an empty list. +/// A SQLite :memory: database +/// () backed +/// by a single shared held open +/// for the lifetime of the host. SQLite enforces real foreign +/// keys and real transactional semantics, so the tests see the +/// same INSERT-time FK validation a production Postgres host +/// would — unlike the EF Core InMemory provider, which silently +/// ignores FKs and masks bugs that surface only against a real +/// relational engine. /// A trivial /// stub: the GET index path doesn't read the file system, so any /// implementation is fine. @@ -44,31 +50,61 @@ namespace Yavsc.Blogs.Tests; /// /// No IdentityServer, no SMTP, no static assets — the Org fixture /// owns all of that and we don't need any of it for blog integration -/// tests. +/// tests. Marked so the +/// host is shared across every [Collection("Yavsc Blogs")] +/// test class: one host, one SQLite DB, one Kestrel port. /// +[CollectionDefinition("Yavsc Blogs")] public sealed class BlogsWebServerFixture : WebHostFixture { protected override int HttpsPort => 5103; - private InMemoryDatabaseRoot? _inMemoryRoot; + public long CircleId { get; private set; } + public long PostId { get; private set; } + + // A single SqliteConnection held open at the static level, + // mirroring how Yavsc.Org.Tests.WebServerFixture hoists its + // shared configuration into static slots. Closing the + // connection destroys the in-memory database — so we close + // it only when the last fixture instance is disposed (see + // Dispose below), exactly when WebHostFixture tears down the + // host. + private static SqliteConnection? _sharedSqliteConnection; + private static readonly object _sqliteLock = new(); protected override WebApplication BuildApp(WebApplicationBuilder builder) { - // Use the real ApplicationDbContext with an in-memory store. - // BlogSpotService reads _context.BlogSpot directly, so any - // attempt to mock it would be wasted work; the real service - // against an empty table returns an empty list, which is - // exactly what the first test wants to assert. - // - // Share a single InMemoryDatabaseRoot across the test - // lifetime so POST + GET on the same fixture see the same - // store. Without the root, EF Core's In-Memory provider - // creates independent stores per DbContext in some - // configurations, and the second request would see an - // empty list even after the first wrote a row. - _inMemoryRoot = new InMemoryDatabaseRoot(); + // Open the shared in-memory connection lazily on the first + // fixture construction. Subsequent constructions (xUnit + // creates one fixture instance per IClassFixture) reuse + // the same connection so all DbContexts across all tests + // see the same database. + SqliteConnection sharedConnection; + lock (_sqliteLock) + { + if (_sharedSqliteConnection is null) + { + // Mode=Memory + Cache=Shared gives us a named + // in-memory database that every connection string + // referencing "File:YavscBlogsTests?mode=memory&cache=shared" + // will resolve to the same backing store, as long + // as at least one SqliteConnection stays open + // against it. + _sharedSqliteConnection = new SqliteConnection( + "Data Source=YavscBlogsTests;Mode=Memory;Cache=Shared"); + _sharedSqliteConnection.Open(); + } + sharedConnection = _sharedSqliteConnection; + } + builder.Services.AddDbContext(opt => - opt.UseInMemoryDatabase("Yavsc.Blogs.Tests", _inMemoryRoot)); + // UseSqlite(DbConnection) keeps the connection we just + // opened alive for the DbContext's lifetime, instead of + // letting EF open and close its own. Without this, + // each DbContext would get a fresh connection pointing + // at an empty :memory: store and nothing would persist + // across requests. + opt.UseSqlite(sharedConnection)); // Trivial file-system auth: the GET index path never calls // into it, but the DI container needs an instance. @@ -145,7 +181,7 @@ public sealed class BlogsWebServerFixture : WebHostFixture // remaps long Microsoft claim URIs, not sub). // UserHelpers.GetUserId reads sub directly. NameClaimType = "sub", - RoleClaimType = YavscConstants.RoleClaimType, + RoleClaimType = Yavsc.Constants.RoleClaimType, }; }); @@ -164,10 +200,139 @@ public sealed class BlogsWebServerFixture : WebHostFixture app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); + + // EnsureCreated + seed alice, run once at host startup. + // EnsureCreated is idempotent (creates only the tables that + // don't exist yet) and runs against the shared + // SqliteConnection (Cache=Shared), so every DbContext that + // resolves through this fixture's host sees the same schema. + // We do NOT call EnsureDeleted: the SqliteConnection is held + // open at the static level and closing it destroys the + // :memory: store for every other DbContext — the org + // fixture can afford EnsureDeleted because its store is + // built fresh per fixture, but the blogs fixture's static + // connection outlives a single fixture instance. + using (var seedScope = app.Services.CreateScope()) + { + var db = seedScope.ServiceProvider + .GetRequiredService(); + db.Database.EnsureCreated(); + if (!db.Users.Any(u => u.Id == "alice")) + { + db.Users.Add(new ApplicationUser + { + Id = "alice", + UserName = "alice", + Email = "alice@example.com", + EmailConfirmed = true, + FullName = "Alice Dupont", + Avatar = "/avatars/alice.png", + }); + db.SaveChanges(); + + // Inline the seed of the circle + post. We don't + // call SeedCircle/SeedBlogPost (the instance helpers) + // because those resolve through this.Services, which + // is null until WebHostFixture.InitializeAsync has + // finished wiring the shared slot — i.e. after this + // method returns. Use app.Services directly. + var circle = new Circle + { + OwnerId = "alice", + Name = "test", + Public = true, + }; + db.Circle.Add(circle); + db.SaveChanges(); + CircleId = circle.Id; + + var post = new BlogPost + { + AuthorId = "alice", + Title = "Billet ACL test", + Article = "Test article body.", + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + }; + db.BlogSpot.Add(post); + db.SaveChanges(); + PostId = post.Id; + } + } + await Task.CompletedTask; return app; } + public override void Dispose() + { + try + { + base.Dispose(); + } + finally + { + // Close the shared SQLite connection only when the + // last fixture instance goes away, matching the + // lifetime contract of WebHostFixture.Dispose. We + // rely on base.Dispose's _instanceCount decrement + // having run, so we close only if the host is gone + // (base already nulled _app when count==0). + lock (_sqliteLock) + { + if (_sharedSqliteConnection is not null) + { + // Synchronous close: SQLite's Close() is + // documented as safe to call from a sync + // context and avoids the GetAwaiter().GetResult() + // pattern that's historically caused teardown + // hangs in this repo's async pipeline. + _sharedSqliteConnection.Close(); + _sharedSqliteConnection.Dispose(); + _sharedSqliteConnection = null; + } + } + } + } + + /// Seed an in the shared + /// SQLite store, so tests that POST/PUT/DELETE a + /// BlogPost (whose AuthorId is a FK to + /// AspNetUsers.Id) don't trip the FK constraint that + /// SQLite enforces but the EF Core InMemory provider silently + /// ignored. Idempotent on : a + /// second call for the same id is a no-op (the user already + /// exists). + /// Both the PK id and the login name. + /// The JWT subject in tests is this same string, so seeding + /// this id is enough to make the FK from a + /// BlogPost.AuthorId resolve. + /// Optional hook to fill in fields + /// like FullName / Avatar / EmailConfirmed + /// that downstream tests assert on. + public ApplicationUser SeedUser(string userName, Action? configure = null) + { + using var scope = Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var existing = db.Users.SingleOrDefault(u => u.Id == userName); + if (existing != null) return existing; + + // Email is an alternate key on ApplicationUser; seeding + // it explicitly avoids the InMemory provider's null-claim + // tracking quirk (cf. PublishEndpointTests.ResetDatabase) + // and keeps the column shape realistic for prod. + var user = new ApplicationUser + { + Id = userName, + UserName = userName, + Email = $"{userName}@example.test", + }; + configure?.Invoke(user); + db.Users.Add(user); + db.SaveChanges(); + return user; + } + /// Trivial stub. The /// blog API endpoints exercised by the first tests don't read the /// file system, so the implementation can be a no-op. @@ -180,4 +345,39 @@ public sealed class BlogsWebServerFixture : WebHostFixture { } } + + + + /// Create a circle owned by + /// directly in the SQLite store and return its server-assigned + /// id. + private long SeedCircle(string ownerId, string name, bool isPublic = false) + { + using var scope = Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var circle = new Circle { OwnerId = ownerId, Name = name, Public = isPublic }; + db.Circle.Add(circle); + db.SaveChanges(); + return circle.Id; + } + + /// Create a blog post owned by + /// directly in the SQLite store and return its server-assigned + /// id. + private long SeedBlogPost(string authorId, string title) + { + using var scope = Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var post = new BlogPost + { + AuthorId = authorId, + Title = title, + Article = "Test article body.", + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + }; + db.BlogSpot.Add(post); + db.SaveChanges(); + return post.Id; + } } diff --git a/src/Yavsc.Blogs.Tests/CircleMembersApiTests.cs b/src/Yavsc.Blogs.Tests/CircleMembersApiTests.cs index 5e8040ef..a2367180 100644 --- a/src/Yavsc.Blogs.Tests/CircleMembersApiTests.cs +++ b/src/Yavsc.Blogs.Tests/CircleMembersApiTests.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Yavsc.Models; using Yavsc.Models.Relationship; using Yavsc.Tests.Shared; +using static Yavsc.Constants; namespace Yavsc.Blogs.Tests; @@ -88,7 +89,7 @@ public sealed class CircleMembersApiTests : IClassFixture } private string MembersUrl(long circleId) - => $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{Constants.APIPrefix}/circle/{circleId}/members"; + => $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{APIPrefix}/circle/{circleId}/members"; private HttpClient NewClient(string subject) { diff --git a/src/Yavsc.Blogs.Tests/MappedClaimsBlogsWebServerFixture.cs b/src/Yavsc.Blogs.Tests/MappedClaimsBlogsWebServerFixture.cs index c9d95774..6d84aed5 100644 --- a/src/Yavsc.Blogs.Tests/MappedClaimsBlogsWebServerFixture.cs +++ b/src/Yavsc.Blogs.Tests/MappedClaimsBlogsWebServerFixture.cs @@ -65,8 +65,8 @@ public sealed class MappedClaimsBlogsWebServerFixture : IDisposable ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKey = TestTokenIssuer.SigningKey, - RoleClaimType = YavscConstants.RoleClaimType, - NameClaimType = YavscConstants.NameClaimType, + RoleClaimType = Yavsc.Constants.RoleClaimType, + NameClaimType = Yavsc.Constants.NameClaimType, }; }); diff --git a/src/Yavsc.Blogs.Tests/PublishEndpointTests.cs b/src/Yavsc.Blogs.Tests/PublishEndpointTests.cs index af1a26c6..e767a57a 100644 --- a/src/Yavsc.Blogs.Tests/PublishEndpointTests.cs +++ b/src/Yavsc.Blogs.Tests/PublishEndpointTests.cs @@ -25,7 +25,7 @@ namespace Yavsc.Blogs.Tests; /// in-memory ApplicationDbContext, JWT bearer auth /// via . /// -[Collection("JwtClaimMapping")] +[Collection("Yavsc Blogs")] public sealed class PublishEndpointTests : IClassFixture { private readonly BlogsWebServerFixture _fixture; diff --git a/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj b/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj index 256bdc4d..fc7883ce 100644 --- a/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj +++ b/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Yavsc.Blogs/Constants.cs b/src/Yavsc.Blogs/Constants.cs index 4dbdfb8b..3e499da4 100644 --- a/src/Yavsc.Blogs/Constants.cs +++ b/src/Yavsc.Blogs/Constants.cs @@ -5,6 +5,4 @@ public static class Constants public const string AdminRole = "Admin"; public const string ModeratorRole = "Moderator"; public const string UserRole = "User"; - - public const string APIPrefix = "api/v1"; } diff --git a/src/Yavsc.Blogs/Controllers/BlogAclApiController.cs b/src/Yavsc.Blogs/Controllers/BlogAclApiController.cs index aa81f9d5..a33d75d8 100644 --- a/src/Yavsc.Blogs/Controllers/BlogAclApiController.cs +++ b/src/Yavsc.Blogs/Controllers/BlogAclApiController.cs @@ -1,15 +1,17 @@ -using System.Linq; + using System.Security.Claims; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Yavsc.Abstract.BlogSpot; using Yavsc.Models; using Yavsc.Models.Access; using Yavsc.Server.Helpers; +using static Yavsc.Constants; namespace Yavsc.Blogs.Controllers { [Produces("application/json")] - [Route("api/blogacl")] + [Route(APIPrefix+"/blogacl")] public class BlogAclApiController : Controller { private readonly ApplicationDbContext _context; @@ -24,7 +26,7 @@ namespace Yavsc.Blogs.Controllers /// Blog posts (and therefore their ACLs) are private to their /// author — the API never exposes another user's ACL. /// - // GET: api/blogacl + // GET: api/v1/blogacl [HttpGet] public IEnumerable GetBlogACL() { @@ -68,7 +70,7 @@ namespace Yavsc.Blogs.Controllers return BadRequest(); } - if (!CheckOwner(circleAuthorizationToBlogPost.CircleId)) + if (!await CheckOwnerAsync(circleAuthorizationToBlogPost.CircleId)) { return new ChallengeResult(); } @@ -92,27 +94,42 @@ namespace Yavsc.Blogs.Controllers return new StatusCodeResult(StatusCodes.Status204NoContent); } - private bool CheckOwner (long circleId) + private async Task CheckOwnerAsync (long circleId) { - var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); - var circle = _context.Circle.First(c=>c.Id==circleId); - _context.Entry(circle).State = EntityState.Detached; - return (circle.OwnerId == uid); + if (uid==null) return false; + var circle = await _context.Circle.FirstOrDefaultAsync(c=>c.Id==circleId); + if (circle == null) return false; + return circle.OwnerId == uid; } // POST: api/BlogAclApi [HttpPost] - public async Task PostCircleAuthorizationToBlogPost([FromBody] CircleAuthorizationToBlogPost circleAuthorizationToBlogPost) + public async Task PostCircleAuthorizationToBlogPost( + [FromBody] PostAccessControlRulePayload circleAuthorizationToBlogPost) { if (!ModelState.IsValid) { return BadRequest(ModelState); } - if (!CheckOwner(circleAuthorizationToBlogPost.CircleId)) + // No 500: a missing or zero BlogPostId is a client + // error, not an EF Core FK violation waiting to happen. + // The 2026-08-21 prod 500 was this exact path (PostIt + // sent only circleId, server saw BlogPostId = 0 and + // SaveChangesAsync threw InvalidOperationException). + if (circleAuthorizationToBlogPost.BlogPostId <= 0) + { + return BadRequest("BlogPostId is required and must be > 0."); + } + if (!await CheckOwnerAsync(circleAuthorizationToBlogPost.CircleId)) { return new ChallengeResult(); } - _context.CircleAuthorizationToBlogPost.Add(circleAuthorizationToBlogPost); + CircleAuthorizationToBlogPost entity = new CircleAuthorizationToBlogPost + { + BlogPostId = circleAuthorizationToBlogPost.BlogPostId, + CircleId = circleAuthorizationToBlogPost.CircleId + }; + _context.CircleAuthorizationToBlogPost.Add(entity); try { await _context.SaveChangesAsync(User.GetUserId()); diff --git a/src/Yavsc.Blogs/Controllers/BlogApiController.cs b/src/Yavsc.Blogs/Controllers/BlogApiController.cs index 76aa777e..fcd3a336 100644 --- a/src/Yavsc.Blogs/Controllers/BlogApiController.cs +++ b/src/Yavsc.Blogs/Controllers/BlogApiController.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Yavsc.Blogspot; using Yavsc.Server.Exceptions; using Yavsc.Server.Helpers; -using static Yavsc.Blogs.Constants; +using static Yavsc.Constants; namespace Yavsc.Blogs.Controllers { diff --git a/src/Yavsc.Blogs/Controllers/BlogTagsApiController.cs b/src/Yavsc.Blogs/Controllers/BlogTagsApiController.cs index a5d905eb..ad6a0893 100644 --- a/src/Yavsc.Blogs/Controllers/BlogTagsApiController.cs +++ b/src/Yavsc.Blogs/Controllers/BlogTagsApiController.cs @@ -1,12 +1,8 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Yavsc.Models; using Yavsc.Models.Blog; -using static Yavsc.Blogs.Constants; +using static Yavsc.Constants; namespace Yavsc.Blogs.Controllers { [Produces("application/json")] diff --git a/src/Yavsc.Blogs/Controllers/CircleApiController.cs b/src/Yavsc.Blogs/Controllers/CircleApiController.cs index 73bbfff9..ea3c981b 100644 --- a/src/Yavsc.Blogs/Controllers/CircleApiController.cs +++ b/src/Yavsc.Blogs/Controllers/CircleApiController.cs @@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore; using Yavsc.Models; using Yavsc.Models.Relationship; using Yavsc.Server.Helpers; -using static Yavsc.Blogs.Constants; +using static Yavsc.Constants; namespace Yavsc.Blogs.Controllers { diff --git a/src/Yavsc.Blogs/Controllers/CommentsApiController.cs b/src/Yavsc.Blogs/Controllers/CommentsApiController.cs index b9f334dc..d4c80f99 100644 --- a/src/Yavsc.Blogs/Controllers/CommentsApiController.cs +++ b/src/Yavsc.Blogs/Controllers/CommentsApiController.cs @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore; using Yavsc.Models; using Yavsc.Models.Blog; using Yavsc.Server.Helpers; -using static Yavsc.Blogs.Constants; +using static Yavsc.Constants; namespace Yavsc.Blogs.Controllers { diff --git a/src/Yavsc.Blogs/Controllers/FileSystemApiController.cs b/src/Yavsc.Blogs/Controllers/FileSystemApiController.cs index 5b067c1b..5e834503 100644 --- a/src/Yavsc.Blogs/Controllers/FileSystemApiController.cs +++ b/src/Yavsc.Blogs/Controllers/FileSystemApiController.cs @@ -2,7 +2,7 @@ using System.Security.Claims; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using static Yavsc.Blogs.Constants; +using static Yavsc.Constants; namespace Yavsc.Blogs.Controllers { @@ -21,7 +21,7 @@ namespace Yavsc.Blogs.Controllers private readonly ILogger _logger; public FileSystemApiController(ApplicationDbContext context, - IAuthorizationService authorizationService, + IAuthorizationService authorizationService, ILoggerFactory loggerFactory) { @@ -38,7 +38,7 @@ namespace Yavsc.Blogs.Controllers [HttpGet("{*subdir}")] public IActionResult GetDir([ValidRemoteUserFilePath] string subdir="") - { + { if (!ModelState.IsValid) return new BadRequestObjectResult(ModelState); // _logger.LogInformation($"listing files from {User.Identity.Name}{subdir}"); var files = AbstractFileSystemHelpers.GetUserFiles(User.GetUserId(), subdir); @@ -57,20 +57,20 @@ namespace Yavsc.Blogs.Controllers } catch (InvalidPathException ex) { pathex = ex; } - if (pathex!=null) + if (pathex!=null) { _logger.LogError($"invalid sub path: '{subdir}'."); return BadRequest(pathex); } _logger.LogInformation($"Receiving files, saved in '{destDir}' (specified as '{subdir}')."); - + var uid = User.GetUserId(); var user = dbContext.Users.Single( u => u.Id == uid ); int i=0; _logger.LogInformation($"Receiving {Request.Form.Files.Count} files."); - + foreach (var f in Request.Form.Files) { var item = user.ReceiveUserFile(destDir, f); @@ -178,7 +178,7 @@ namespace Yavsc.Blogs.Controllers return Ok(new { deleted=id }); } - + } } diff --git a/src/Yavsc.Blogs/Controllers/FileSystemStreamController.cs b/src/Yavsc.Blogs/Controllers/FileSystemStreamController.cs index 23cf0cc6..bc6485dd 100644 --- a/src/Yavsc.Blogs/Controllers/FileSystemStreamController.cs +++ b/src/Yavsc.Blogs/Controllers/FileSystemStreamController.cs @@ -8,7 +8,7 @@ using Yavsc.Models.Messaging; using Yavsc.Services; using Microsoft.AspNetCore.SignalR; using Yavsc.Server.Helpers; -using static Yavsc.Blogs.Constants; +using static Yavsc.Constants; using Yavsc.Server.Hubs; namespace Yavsc.Blogs.Controllers diff --git a/src/Yavsc.Blogs/Controllers/PostTagsApiController.cs b/src/Yavsc.Blogs/Controllers/PostTagsApiController.cs index e908edec..da03c19c 100644 --- a/src/Yavsc.Blogs/Controllers/PostTagsApiController.cs +++ b/src/Yavsc.Blogs/Controllers/PostTagsApiController.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Mvc; -using static Yavsc.Blogs.Constants; +using static Yavsc.Constants; namespace Yavsc.Blogs.Controllers { diff --git a/src/Yavsc.Blogs/Controllers/TagsApiController.cs b/src/Yavsc.Blogs/Controllers/TagsApiController.cs index daf0220b..d4c2b538 100644 --- a/src/Yavsc.Blogs/Controllers/TagsApiController.cs +++ b/src/Yavsc.Blogs/Controllers/TagsApiController.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Yavsc.Models; -using static Yavsc.Blogs.Constants; +using static Yavsc.Constants; namespace Yavsc.Controllers { diff --git a/src/Yavsc.Blogs/Controllers/UserSearchApiController.cs b/src/Yavsc.Blogs/Controllers/UserSearchApiController.cs index 048db154..951a5880 100644 --- a/src/Yavsc.Blogs/Controllers/UserSearchApiController.cs +++ b/src/Yavsc.Blogs/Controllers/UserSearchApiController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Yavsc.Models; +using static Yavsc.Constants; namespace Yavsc.Blogs.Controllers { @@ -26,7 +27,7 @@ namespace Yavsc.Blogs.Controllers /// exposing it. /// [Produces("application/json")] - [Route( Constants.APIPrefix + "/user-search")] + [Route(APIPrefix + "/user-search")] [Authorize] public class UserSearchApiController : Controller { @@ -66,8 +67,9 @@ namespace Yavsc.Blogs.Controllers // book callers already know the email they're // searching for and we don't want to surface a // long tail of partial matches. - var normalised = e.Trim(); - query = query.Where(u => u.Email != null && u.Email.ToLower() == normalised.ToLower()); + var normalized = e.Trim(); + query = query.Where(u => u.Email != null && + string.Compare(u.Email, normalized, true) ==0); } if (!string.IsNullOrWhiteSpace(q)) diff --git a/src/Yavsc.Blogs/Program.cs b/src/Yavsc.Blogs/Program.cs index 742c9eee..952115d3 100644 --- a/src/Yavsc.Blogs/Program.cs +++ b/src/Yavsc.Blogs/Program.cs @@ -51,7 +51,7 @@ internal class Program // DbContextBuilder services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString( - YavscConstants.YavscConnectionStringName))); + Yavsc.Constants.YavscConnectionStringName))); // other services services diff --git a/src/Yavsc.Org.Tests/NonRegression/EMailling.cs b/src/Yavsc.Org.Tests/NonRegression/EMailling.cs index 453c2650..743dbbc1 100644 --- a/src/Yavsc.Org.Tests/NonRegression/EMailling.cs +++ b/src/Yavsc.Org.Tests/NonRegression/EMailling.cs @@ -19,11 +19,11 @@ namespace Yavsc.Org.Tests { this.output = output; _serverFixture = serverFixture; - _logger = serverFixture.Logger; + _logger = serverFixture.Logger!; } [Fact] - public void SendEMailSynchrone() + public async Task SendEMailSynchrone() { using IServiceScope scope = _serverFixture.Services.CreateScope(); @@ -32,12 +32,12 @@ namespace Yavsc.Org.Tests scope.ServiceProvider.GetRequiredService()); output.WriteLine("SendEMailSynchrone ..."); - mailSender.SendEmailAsync + await mailSender.SendEmailAsync ( - _serverFixture.SiteSettings.Owner.Name, - _serverFixture.SiteSettings.Owner.EMail, + _serverFixture.SiteSettings!.Owner.Name, + _serverFixture.SiteSettings!.Owner.EMail, $"monthly email", - "test boby monthly email").Wait(); + "test boby monthly email"); // Assert the SMTP roundtrip was short-circuited by the // recording fake installed in WebServerFixture: exactly diff --git a/src/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs b/src/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs index a0249fa4..7543a42e 100644 --- a/src/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs +++ b/src/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs @@ -14,7 +14,7 @@ namespace Yavsc.Org.Tests.NonRegression; /// ne voit rien — juste un 500 muet. /// /// Le fix passe par qui -/// retourne pour toute +/// retourne pour toute /// donnée partielle. Ces tests couvrent les trois formes de /// "donnée absente" : user null, UserName vide, UserName whitespace. /// @@ -23,21 +23,21 @@ public class UserDisplayHelpersTests [Fact] public void AvatarSrc_null_user_returns_default_avatar() { - Assert.Equal(YavscConstants.DefaultAvatar, UserDisplayHelpers.AvatarSrc(null)); + Assert.Equal(Yavsc.Constants.DefaultAvatar, UserDisplayHelpers.AvatarSrc(null)); } [Fact] public void AvatarSrc_user_with_empty_UserName_returns_default_avatar() { var user = new FakeUser { UserName = "" }; - Assert.Equal(YavscConstants.DefaultAvatar, UserDisplayHelpers.AvatarSrc(user)); + Assert.Equal(Yavsc.Constants.DefaultAvatar, UserDisplayHelpers.AvatarSrc(user)); } [Fact] public void AvatarSrc_user_with_whitespace_UserName_returns_default_avatar() { var user = new FakeUser { UserName = " " }; - Assert.Equal(YavscConstants.DefaultAvatar, UserDisplayHelpers.AvatarSrc(user)); + Assert.Equal(Yavsc.Constants.DefaultAvatar, UserDisplayHelpers.AvatarSrc(user)); } [Fact] @@ -47,7 +47,7 @@ public class UserDisplayHelpersTests // Le path doit matcher YavscConstants.AvatarsPath (minuscule), // pas un /Avatars/ avec S majuscule qui ne résout pas // dans le middleware de fichiers statiques. - var expected = $"{YavscConstants.AvatarsPath}/alice.s.png"; + var expected = $"{Yavsc.Constants.AvatarsPath}/alice.s.png"; Assert.Equal(expected, UserDisplayHelpers.AvatarSrc(user)); } diff --git a/src/Yavsc.Org.Tests/WebServerFixture.cs b/src/Yavsc.Org.Tests/WebServerFixture.cs index a623bc9e..edfd87d9 100644 --- a/src/Yavsc.Org.Tests/WebServerFixture.cs +++ b/src/Yavsc.Org.Tests/WebServerFixture.cs @@ -80,8 +80,8 @@ public sealed class WebServerFixture : WebHostFixture // can resolve it. The AddConfiguration extension takes care of // that plus the in-memory overrides below. builder.AddConfiguration(null).AddInMemoryCollection(new Dictionary - { - [$"ConnectionStrings:{YavscConstants.YavscConnectionStringName}"] = "InMemory", + { + [$"ConnectionStrings:{Yavsc.Constants.YavscConnectionStringName}"] = "InMemory", // SMTP test config: UserName non-null so MailSender // exercises the Authenticate branch — the // RecordingSmtpClient captures it. diff --git a/src/Yavsc.Org/Controllers/Accounting/AccountController.cs b/src/Yavsc.Org/Controllers/Accounting/AccountController.cs index 43565442..c562736d 100644 --- a/src/Yavsc.Org/Controllers/Accounting/AccountController.cs +++ b/src/Yavsc.Org/Controllers/Accounting/AccountController.cs @@ -90,7 +90,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, "ConfirmYourAccountTitle" }) Debug.Assert(!_localizer[name].ResourceNotFound); - + } @@ -116,7 +116,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, { await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName, clientId: context?.Client.ClientId)); - // only set explicit expiration here if user chooses "remember me". + // only set explicit expiration here if user chooses "remember me". // otherwise we rely upon expiration configured in cookie middleware. await HttpContext.SignInAsync(user, _roleManager, model.RememberMe, _dbContext); var authResult = await HttpContext.AuthenticateAsync(); @@ -198,7 +198,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, /// /// Entry point into the login workflow /// - [HttpGet(YavscConstants.SigninPath)] + [HttpGet(Constants.SigninPath)] public async Task Signin(SignInModel model) { // build a model so we know what to show on the login page @@ -216,11 +216,11 @@ IHtmlLocalizerFactory htmlLocalizerFactory, /// /// Handle postback from username/password login /// - /// - [HttpPost(YavscConstants.SigninPath)] + /// + [HttpPost(Constants.SigninPath)] [ValidateAntiForgeryToken] [AllowAnonymous] - + public async Task Signin([FromForm] SignInModel model, [FromForm] string button) { @@ -232,7 +232,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, { if (context != null) { - // if the user cancels, send a result back into IdentityServer as if they + // if the user cancels, send a result back into IdentityServer as if they // denied the consent (even if this client does not require consent). // this will send back an access denied OIDC error response to the client. await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied); @@ -269,7 +269,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, { await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName, clientId: context?.Client.ClientId)); - // only set explicit expiration here if user chooses "remember me". + // only set explicit expiration here if user chooses "remember me". // otherwise we rely upon expiration configured in cookie middleware. await HttpContext.SignInAsync(user, _roleManager, model.RememberMe, _dbContext); @@ -396,7 +396,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, var local = context.IdP == IdentityServer8.IdentityServerConstants.LocalIdentityProvider; // this is meant to short circuit the UI and only trigger the one external IdP - + model.EnableLocalLogin = local; model.UserName = context?.LoginHint; model.IsExternalLoginOnly = false; @@ -579,7 +579,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, // Send an email with this link Uri authority = new Uri(Config.Authority); - + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code }, @@ -659,7 +659,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, } // // POST: /Account/LogOff - [HttpPost(YavscConstants.LogoutPath)] + [HttpPost(Constants.LogoutPath)] [ValidateAntiForgeryToken] public async Task LogOff(string returnUrl = null) { @@ -829,7 +829,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, bool result = false; try { - result = await _userManager.VerifyTwoFactorTokenAsync(user, YavscConstants.DefaultFactor, code); + result = await _userManager.VerifyTwoFactorTokenAsync(user, Constants.DefaultFactor, code); _dbContext.SaveChanges(userId); } catch (Exception ex) @@ -1024,12 +1024,12 @@ IHtmlLocalizerFactory htmlLocalizerFactory, } // Generate the token and send it - if (model.SelectedProvider == YavscConstants.MobileAppFactor) + if (model.SelectedProvider == Constants.MobileAppFactor) { return View("Error", new Exception("No mobile app service was activated")); } else - if (model.SelectedProvider == YavscConstants.SMSFactor) + if (model.SelectedProvider == Constants.SMSFactor) { return View("Error", new Exception("No SMS service was activated")); // await _smsSender.SendSmsAsync(_twilioSettings, await _userManager.GetPhoneNumberAsync(user), message); diff --git a/src/Yavsc.Org/Controllers/Administration/AdministrationController.cs b/src/Yavsc.Org/Controllers/Administration/AdministrationController.cs index 7104e178..e2944e9e 100644 --- a/src/Yavsc.Org/Controllers/Administration/AdministrationController.cs +++ b/src/Yavsc.Org/Controllers/Administration/AdministrationController.cs @@ -50,12 +50,12 @@ namespace Yavsc.Controllers { // ensure all roles existence foreach (string roleName in new string[] { - YavscConstants.AdminGroupName, - YavscConstants.StarGroupName, - YavscConstants.PerformerGroupName, - YavscConstants.FrontOfficeGroupName, - YavscConstants.StarHunterGroupName, - YavscConstants.BlogModeratorGroupName + Constants.AdminGroupName, + Constants.StarGroupName, + Constants.PerformerGroupName, + Constants.FrontOfficeGroupName, + Constants.StarHunterGroupName, + Constants.BlogModeratorGroupName }) if (!await _roleManager.RoleExistsAsync(roleName)) { @@ -80,11 +80,11 @@ namespace Yavsc.Controllers public async Task Take() { // If some amdin already exists, make this method disapear - var admins = await _userManager.GetUsersInRoleAsync(YavscConstants.AdminGroupName); + var admins = await _userManager.GetUsersInRoleAsync(Constants.AdminGroupName); if (admins != null && admins.Count > 0) { // All is ok, nothing to do here. - if (User.IsInMsRole(YavscConstants.AdminGroupName)) + if (User.IsInMsRole(Constants.AdminGroupName)) { return Ok(new { message = "you already got it." }); @@ -100,7 +100,7 @@ namespace Yavsc.Controllers return new BadRequestObjectResult(ModelState); } - var addToRoleResult = await _userManager.AddToRoleAsync(user, YavscConstants.AdminGroupName); + var addToRoleResult = await _userManager.AddToRoleAsync(user, Constants.AdminGroupName); if (!addToRoleResult.Succeeded) { AddErrors(addToRoleResult); @@ -114,11 +114,11 @@ namespace Yavsc.Controllers public async Task Index() { var adminCount = await _userManager.GetUsersInRoleAsync( - YavscConstants.AdminGroupName); + Constants.AdminGroupName); var userCount = await _dbContext.Users.CountAsync(); var youAreAdmin = await _userManager.IsInRoleAsync( await _userManager.FindByIdAsync(User.GetUserId()), - YavscConstants.AdminGroupName); + Constants.AdminGroupName); var roles = await _roleManager.Roles.Select(x => new RoleInfo { diff --git a/src/Yavsc.Org/Controllers/Administration/ApiScopesApiController.cs b/src/Yavsc.Org/Controllers/Administration/ApiScopesApiController.cs index 983e7233..d6e83c36 100644 --- a/src/Yavsc.Org/Controllers/Administration/ApiScopesApiController.cs +++ b/src/Yavsc.Org/Controllers/Administration/ApiScopesApiController.cs @@ -1,13 +1,13 @@ using IdentityServer8.EntityFramework.Entities; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Yavsc.Models; using Yavsc.Server.Helpers; +using static Yavsc.Constants; namespace Yavsc.Org.Controllers.Administration { - [Route("api/[controller]")] + [Route(APIPrefix + "/[controller]")] [ApiController] public class ApiScopesApiController : ControllerBase { diff --git a/src/Yavsc.Org/Controllers/Communicating/AnnouncesController.cs b/src/Yavsc.Org/Controllers/Communicating/AnnouncesController.cs index e5002168..f8b41c99 100644 --- a/src/Yavsc.Org/Controllers/Communicating/AnnouncesController.cs +++ b/src/Yavsc.Org/Controllers/Communicating/AnnouncesController.cs @@ -18,11 +18,11 @@ namespace Yavsc.Controllers readonly IStringLocalizer _localizer; readonly IAuthorizationService _authorizationService; - public AnnouncesController(ApplicationDbContext context, + public AnnouncesController(ApplicationDbContext context, IAuthorizationService authorizationService, IStringLocalizer localizer) { - _context = context; + _context = context; _authorizationService = authorizationService; _localizer = localizer; } @@ -59,16 +59,16 @@ namespace Yavsc.Controllers } private async Task SetupView(Announce announce) { - ViewBag.IsAdmin = User.IsInMsRole(YavscConstants.AdminGroupName); - ViewBag.IsPerformer = User.IsInMsRole(YavscConstants.PerformerGroupName); + ViewBag.IsAdmin = User.IsInMsRole(Constants.AdminGroupName); + ViewBag.IsPerformer = User.IsInMsRole(Constants.PerformerGroupName); ViewBag.AllowEdit = announce==null || announce.Id<=0 || !_authorizationService.AuthorizeAsync(User,announce,new EditPermission()).IsFaulted; List dl = new List(); var rnames = System.Enum.GetNames(typeof(Reason)); var rvalues = System.Enum.GetValues(typeof(Reason)); - + for (int i = 0; i a.Children).FirstOrDefault(a => a.Code == code); @@ -123,9 +123,9 @@ namespace Yavsc.Controllers [ValidateAntiForgeryToken] public IActionResult Create(Activity activity) { - if (activity.ParentCode==YavscConstants.NoneCode) + if (activity.ParentCode==Constants.NoneCode) activity.ParentCode=null; - if (activity.SettingsClassName==YavscConstants.NoneCode) + if (activity.SettingsClassName==Constants.NoneCode) activity.SettingsClassName=null; if (ModelState.IsValid) @@ -161,9 +161,9 @@ namespace Yavsc.Controllers [ValidateAntiForgeryToken] public IActionResult Edit(Activity activity) { - if (activity.ParentCode==YavscConstants.NoneCode) + if (activity.ParentCode==Constants.NoneCode) activity.ParentCode=null; - if (activity.SettingsClassName==YavscConstants.NoneCode) + if (activity.SettingsClassName==Constants.NoneCode) activity.SettingsClassName=null; if (ModelState.IsValid) { diff --git a/src/Yavsc.Org/Controllers/DimissClicksApiController.cs b/src/Yavsc.Org/Controllers/DimissClicksApiController.cs index 19f90787..b07bc4b3 100644 --- a/src/Yavsc.Org/Controllers/DimissClicksApiController.cs +++ b/src/Yavsc.Org/Controllers/DimissClicksApiController.cs @@ -10,7 +10,7 @@ using Yavsc.Server.Helpers; namespace Yavsc.Controllers { [Produces("application/json")] - [Route("api/v1/dimiss")] + [Route(Constants.APIPrefix + "/v1/dimiss")] public class DimissClicksApiController : Controller { private readonly ApplicationDbContext _context; @@ -140,7 +140,7 @@ namespace Yavsc.Controllers var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); if (!User.IsInRole("Administrator")) if (uid != id) return new ChallengeResult(); - + if (!ModelState.IsValid) { return BadRequest(ModelState); diff --git a/src/Yavsc.Org/Controllers/HomeController.cs b/src/Yavsc.Org/Controllers/HomeController.cs index 67c19fed..c7aa131f 100644 --- a/src/Yavsc.Org/Controllers/HomeController.cs +++ b/src/Yavsc.Org/Controllers/HomeController.cs @@ -20,10 +20,10 @@ namespace Yavsc.Controllers readonly IHtmlLocalizer _localizer; private SiteSettings siteSettings; - public HomeController(ILogger logger, - IHtmlLocalizer localizer, + public HomeController(ILogger logger, + IHtmlLocalizer localizer, ApplicationDbContext context, - IOptions settingsOptions, + IOptions settingsOptions, IWebHostEnvironment env ) { @@ -37,9 +37,9 @@ namespace Yavsc.Controllers public async Task Index(string id) { - ViewBag.IsFromSecureProx = Request.Headers.ContainsKey(YavscConstants.SshHeaderKey) && Request.Headers[YavscConstants.SshHeaderKey] == "on"; + ViewBag.IsFromSecureProx = Request.Headers.ContainsKey(Constants.SshHeaderKey) && Request.Headers[Constants.SshHeaderKey] == "on"; ViewBag.SecureHomeUrl = "https://" + Request.Headers["X-Forwarded-Host"]; - ViewBag.SshHeaderKey = Request.Headers[YavscConstants.SshHeaderKey]; + ViewBag.SshHeaderKey = Request.Headers[Constants.SshHeaderKey]; var uid = User.GetUserId(); long[] clicked = null; if (uid == null) @@ -140,8 +140,8 @@ namespace Yavsc.Controllers errorViewModel.Description ??= string.Empty; errorViewModel.Description += " Page: Home."; } - - + + return View("~/Views/Shared/Error.cshtml", errorViewModel); } public IActionResult Status(int id) diff --git a/src/Yavsc.Org/Controllers/Musical/InstrumentationController.cs b/src/Yavsc.Org/Controllers/Musical/InstrumentationController.cs index dc905d08..536c7417 100644 --- a/src/Yavsc.Org/Controllers/Musical/InstrumentationController.cs +++ b/src/Yavsc.Org/Controllers/Musical/InstrumentationController.cs @@ -17,7 +17,7 @@ namespace Yavsc.Controllers public InstrumentationController(ApplicationDbContext context) { - _context = context; + _context = context; } // GET: Instrumentation @@ -50,7 +50,7 @@ namespace Yavsc.Controllers var owned = _context.Instrumentation.Include(i=>i.Tool).Where(i=>i.UserId==uid).Select(i=>i.InstrumentId); var ownedArray = owned.ToArray(); - ViewBag.YetAvailableInstruments = _context.Instrument.Select(k=>new SelectListItem + ViewBag.YetAvailableInstruments = _context.Instrument.Select(k=>new SelectListItem { Text = k.Name, Value = k.Id.ToString(), Disabled = ownedArray.Contains(k.Id) }); return View(new Instrumentation { UserId = uid }); @@ -64,7 +64,7 @@ namespace Yavsc.Controllers var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); if (ModelState.IsValid) { - if (model.UserId != uid) if (!User.IsInMsRole(YavscConstants.AdminGroupName)) + if (model.UserId != uid) if (!User.IsInMsRole(Constants.AdminGroupName)) return new ChallengeResult(); _context.Instrumentation.Add(model); @@ -82,7 +82,7 @@ namespace Yavsc.Controllers { return NotFound(); } - if (id != uid) if (!User.IsInMsRole(YavscConstants.AdminGroupName)) + if (id != uid) if (!User.IsInMsRole(Constants.AdminGroupName)) return new ChallengeResult(); Instrumentation musicianSettings = await _context.Instrumentation.SingleAsync(m => m.UserId == id); if (musicianSettings == null) @@ -98,7 +98,7 @@ namespace Yavsc.Controllers public async Task Edit(Instrumentation musicianSettings) { var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (musicianSettings.UserId != uid) if (!User.IsInMsRole(YavscConstants.AdminGroupName)) + if (musicianSettings.UserId != uid) if (!User.IsInMsRole(Constants.AdminGroupName)) return new ChallengeResult(); if (ModelState.IsValid) { @@ -124,7 +124,7 @@ namespace Yavsc.Controllers return NotFound(); } var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (musicianSettings.UserId != uid) if (!User.IsInMsRole(YavscConstants.AdminGroupName)) + if (musicianSettings.UserId != uid) if (!User.IsInMsRole(Constants.AdminGroupName)) return new ChallengeResult(); return View(musicianSettings); } @@ -135,12 +135,12 @@ namespace Yavsc.Controllers public async Task DeleteConfirmed(string id) { Instrumentation musicianSettings = await _context.Instrumentation.SingleAsync(m => m.UserId == id); - + var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (musicianSettings.UserId != uid) if (!User.IsInMsRole(YavscConstants.AdminGroupName)) + if (musicianSettings.UserId != uid) if (!User.IsInMsRole(Constants.AdminGroupName)) return new ChallengeResult(); - + _context.Instrumentation.Remove(musicianSettings); await _context.SaveChangesAsync(User.GetUserId()); return RedirectToAction("Index"); diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs index 0cae23a7..f92dc3c9 100644 --- a/src/Yavsc.Org/Extensions/HostingExtensions.cs +++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs @@ -169,7 +169,7 @@ public static class HostingExtensions public static IdentityBuilder AddIdentityDBAndStores(this WebApplicationBuilder builder) { IServiceCollection services = builder.Services; - var connectionString = builder.Configuration.GetConnectionString(YavscConstants.YavscConnectionStringName); + var connectionString = builder.Configuration.GetConnectionString(Constants.YavscConnectionStringName); services.AddDbContext(options => { @@ -197,7 +197,7 @@ public static class HostingExtensions options.SignIn.RequireConfirmedAccount = builder.Environment.IsEnvironment( builder.Environment.EnvironmentName); options.ClaimsIdentity.UserNameClaimType = JwtClaimTypes.PreferredUserName; - options.ClaimsIdentity.RoleClaimType = YavscConstants.RoleClaimType; + options.ClaimsIdentity.RoleClaimType = Constants.RoleClaimType; } ) .AddEntityFrameworkStores(); @@ -239,18 +239,18 @@ public static class HostingExtensions { policy .RequireAuthenticatedUser() - .RequireClaim(YavscConstants.RoleClaimType, - new string[] { YavscConstants.PerformerGroupName, YavscConstants.AdminGroupName }) + .RequireClaim(Constants.RoleClaimType, + new string[] { Constants.PerformerGroupName, Constants.AdminGroupName }) ; }); options.AddPolicy("AdministratorOnly", policy => { _ = policy .RequireAuthenticatedUser() - .RequireClaim(YavscConstants.RoleClaimType, YavscConstants.AdminGroupName); + .RequireClaim(Constants.RoleClaimType, Constants.AdminGroupName); }); - options.AddPolicy("FrontOffice", policy => policy.RequireRole(YavscConstants.FrontOfficeGroupName)); + options.AddPolicy("FrontOffice", policy => policy.RequireRole(Constants.FrontOfficeGroupName)); // options.AddPolicy("EmployeeId", policy => policy.RequireClaim("EmployeeId", "123", "456")); // options.AddPolicy("BuildingEntry", policy => policy.Requirements.Add(new OfficeEntryRequirement())); @@ -314,10 +314,10 @@ public static class HostingExtensions { options.ClaimsIdentity.UserIdClaimType = JwtClaimTypes.Subject; options.ClaimsIdentity.UserNameClaimType = JwtClaimTypes.Name; - options.ClaimsIdentity.RoleClaimType = YavscConstants.RoleClaimType; + options.ClaimsIdentity.RoleClaimType = Constants.RoleClaimType; }); var migrationsAssembly = typeof(Program).GetTypeInfo().Assembly.GetName().Name; - var connectionString = builder.Configuration.GetConnectionString(YavscConstants.YavscConnectionStringName); + var connectionString = builder.Configuration.GetConnectionString(Constants.YavscConnectionStringName); string sqliteConnectionString = $"Data Source={Path.Combine(Path.GetTempPath(), "yavsc_test.db")}"; @@ -1220,7 +1220,7 @@ ADD COLUMN IF NOT EXISTS ""Moderated"" boolean NOT NULL DEFAULT FALSE;"); Config.UserFilesOptions = new FileServerOptions() { FileProvider = new PhysicalFileProvider(AbstractFileSystemHelpers.UserFilesDirName), - RequestPath = PathString.FromUriComponent(YavscConstants.UserFilesPath), + RequestPath = PathString.FromUriComponent(Constants.UserFilesPath), EnableDirectoryBrowsing = enableDirectoryBrowsing, }; Config.UserFilesOptions.EnableDefaultFiles = true; @@ -1233,7 +1233,7 @@ ADD COLUMN IF NOT EXISTS ""Moderated"" boolean NOT NULL DEFAULT FALSE;"); Config.AvatarsOptions = new FileServerOptions() { FileProvider = new PhysicalFileProvider(Config.AvatarsDirName), - RequestPath = PathString.FromUriComponent(YavscConstants.AvatarsPath), + RequestPath = PathString.FromUriComponent(Constants.AvatarsPath), EnableDirectoryBrowsing = enableDirectoryBrowsing }; @@ -1244,7 +1244,7 @@ ADD COLUMN IF NOT EXISTS ""Moderated"" boolean NOT NULL DEFAULT FALSE;"); Config.GitOptions = new FileServerOptions() { FileProvider = new PhysicalFileProvider(Config.GitDirName), - RequestPath = PathString.FromUriComponent(YavscConstants.GitPath), + RequestPath = PathString.FromUriComponent(Constants.GitPath), EnableDirectoryBrowsing = enableDirectoryBrowsing, }; Config.GitOptions.DefaultFilesOptions.DefaultFileNames.Add("index.md"); diff --git a/src/Yavsc.Org/Migrations/20260820232152_DropCommentFromCircleAuthorizationToBlogPost.Designer.cs b/src/Yavsc.Org/Migrations/20260820232152_DropCommentFromCircleAuthorizationToBlogPost.Designer.cs new file mode 100644 index 00000000..a9af372c --- /dev/null +++ b/src/Yavsc.Org/Migrations/20260820232152_DropCommentFromCircleAuthorizationToBlogPost.Designer.cs @@ -0,0 +1,4645 @@ +// +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("20260820232152_DropCommentFromCircleAuthorizationToBlogPost")] + partial class DropCommentFromCircleAuthorizationToBlogPost + { + /// + 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("EMail") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("ClientProviderInfo"); + }); + + modelBuilder.Entity("Yavsc.Abstract.Models.Messaging.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Target") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("body") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("click_action") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("color") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("icon") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasDefaultValue("exclam"); + + b.Property("sound") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("tag") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("title") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Notification"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("TargetId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TargetId"); + + b.ToTable("Ban"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.BlackListed", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("BlackListed"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToBlogPost", b => + { + b.Property("CircleId") + .HasColumnType("bigint"); + + b.Property("BlogPostId") + .HasColumnType("bigint"); + + b.HasKey("CircleId", "BlogPostId"); + + b.HasIndex("BlogPostId"); + + b.ToTable("CircleAuthorizationToBlogPost"); + }); + + modelBuilder.Entity("Yavsc.Models.AccountBalance", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("ContactCredits") + .HasColumnType("bigint"); + + b.Property("Credits") + .HasColumnType("numeric"); + + b.HasKey("UserId"); + + b.ToTable("BankStatus"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("AllowMonthlyEmail") + .HasColumnType("boolean"); + + b.Property("Avatar") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasDefaultValue("/images/Users/icon_user.png"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("DedicatedGoogleCalendar") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("DiskQuota") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(524288000L); + + b.Property("DiskUsage") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FullName") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxFileSize") + .HasColumnType("bigint"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PostalAddressId") + .HasColumnType("bigint"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Email"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("PostalAddressId"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Yavsc.Models.BalanceImpact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BalanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExecDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Impact") + .HasColumnType("numeric"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BalanceId"); + + b.ToTable("BalanceImpact"); + }); + + modelBuilder.Entity("Yavsc.Models.Bank.BankIdentity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountNumber") + .HasColumnType("text"); + + b.Property("BIC") + .HasColumnType("text"); + + b.Property("BankCode") + .HasColumnType("text"); + + b.Property("BankedKey") + .HasColumnType("integer"); + + b.Property("IBAN") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("WicketCode") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("BankIdentity"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.CommandLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Count") + .HasColumnType("integer"); + + b.Property("Currency") + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.Property("EstimateTemplateId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UnitaryCost") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EstimateId"); + + b.HasIndex("EstimateTemplateId"); + + b.ToTable("CommandLine"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttachedFilesString") + .HasColumnType("text"); + + b.Property("AttachedGraphicsString") + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CommandId") + .HasColumnType("bigint"); + + b.Property("CommandType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("ProviderValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("CommandId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Estimates"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EstimateTemplates"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.ExceptionSIREN", b => + { + b.Property("SIREN") + .HasColumnType("text"); + + b.HasKey("SIREN"); + + b.ToTable("ExceptionsSIREN"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Signature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CapturedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CoordinateMax") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(10000); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignerId") + .IsRequired() + .HasColumnType("text"); + + b.PrimitiveCollection("Strokes") + .IsRequired() + .HasColumnType("integer[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SignerId"); + + b.HasIndex("EstimateId", "Type") + .IsUnique(); + + b.ToTable("Signatures"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b => + { + b.Property("FileId") + .HasColumnType("bigint"); + + b.Property("PostId") + .HasColumnType("bigint"); + + b.HasKey("FileId", "PostId"); + + b.HasIndex("PostId"); + + b.ToTable("BlogAttachedFiles"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Article") + .HasMaxLength(56224) + .HasColumnType("character varying(56224)"); + + b.Property("AuthorId") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Photo") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("BlogSpot"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogTag", b => + { + b.Property("PostId") + .HasColumnType("bigint"); + + b.Property("TagId") + .HasColumnType("bigint"); + + b.HasKey("PostId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("BlogTag"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Article") + .HasColumnType("text"); + + b.Property("AuthorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ParentId") + .HasColumnType("bigint"); + + b.Property("ReceiverId") + .HasColumnType("bigint"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.Property("Visible") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ParentId"); + + b.HasIndex("ReceiverId"); + + b.ToTable("Comment"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.UploadedFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContentType") + .HasColumnType("text"); + + b.Property("Length") + .HasColumnType("bigint"); + + b.Property("Path") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("UploadedFiles"); + }); + + modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => + { + b.Property("BlogpostId") + .HasColumnType("bigint"); + + b.HasKey("BlogpostId"); + + b.ToTable("blogSpotPublications"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.Property("OwnerId") + .HasColumnType("text"); + + b.HasKey("OwnerId"); + + b.ToTable("Schedule"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.ScheduledEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PeriodEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PeriodStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Reccurence") + .HasColumnType("integer"); + + b.Property("ScheduleOwnerId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScheduleOwnerId"); + + b.HasIndex("PeriodStart", "PeriodEnd"); + + b.ToTable("ScheduledEvent"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatConnection", b => + { + b.Property("ConnectionId") + .HasColumnType("text"); + + b.Property("ApplicationUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Connected") + .HasColumnType("boolean"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.HasKey("ConnectionId"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("ChatConnection"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.Property("Name") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LatestJoinPart") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Topic") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Name"); + + b.HasIndex("OwnerId"); + + b.ToTable("ChatRoom"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoomAccess", b => + { + b.Property("ChannelName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("ChannelName", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ChatRoomAccess"); + }); + + modelBuilder.Entity("Yavsc.Models.Cratie.Option", b => + { + b.Property("Code") + .HasColumnType("text"); + + b.Property("CodeScrutin") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Code", "CodeScrutin"); + + b.ToTable("Option"); + }); + + modelBuilder.Entity("Yavsc.Models.Drawing.Color", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Blue") + .HasColumnType("smallint"); + + b.Property("Green") + .HasColumnType("smallint"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Red") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("Color"); + }); + + modelBuilder.Entity("Yavsc.Models.Forms.Form", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Summary") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Form"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("ActionDistance") + .HasColumnType("integer"); + + b.Property("CarePrice") + .HasColumnType("numeric"); + + b.Property("FlatFeeDiscount") + .HasColumnType("numeric"); + + b.Property("HalfBalayagePrice") + .HasColumnType("numeric"); + + b.Property("HalfBrushingPrice") + .HasColumnType("numeric"); + + b.Property("HalfColorPrice") + .HasColumnType("numeric"); + + b.Property("HalfDefrisPrice") + .HasColumnType("numeric"); + + b.Property("HalfFoldingPrice") + .HasColumnType("numeric"); + + b.Property("HalfMechPrice") + .HasColumnType("numeric"); + + b.Property("HalfMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("HalfPermanentPrice") + .HasColumnType("numeric"); + + b.Property("KidCutPrice") + .HasColumnType("numeric"); + + b.Property("LongBalayagePrice") + .HasColumnType("numeric"); + + b.Property("LongBrushingPrice") + .HasColumnType("numeric"); + + b.Property("LongColorPrice") + .HasColumnType("numeric"); + + b.Property("LongDefrisPrice") + .HasColumnType("numeric"); + + b.Property("LongFoldingPrice") + .HasColumnType("numeric"); + + b.Property("LongMechPrice") + .HasColumnType("numeric"); + + b.Property("LongMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("LongPermanentPrice") + .HasColumnType("numeric"); + + b.Property("ManBrushPrice") + .HasColumnType("numeric"); + + b.Property("ManCutPrice") + .HasColumnType("numeric"); + + b.Property("ScheduleOwnerId") + .HasColumnType("text"); + + b.Property("ShampooPrice") + .HasColumnType("numeric"); + + b.Property("ShortBalayagePrice") + .HasColumnType("numeric"); + + b.Property("ShortBrushingPrice") + .HasColumnType("numeric"); + + b.Property("ShortColorPrice") + .HasColumnType("numeric"); + + b.Property("ShortDefrisPrice") + .HasColumnType("numeric"); + + b.Property("ShortFoldingPrice") + .HasColumnType("numeric"); + + b.Property("ShortMechPrice") + .HasColumnType("numeric"); + + b.Property("ShortMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("ShortPermanentPrice") + .HasColumnType("numeric"); + + b.Property("WomenHalfCutPrice") + .HasColumnType("numeric"); + + b.Property("WomenLongCutPrice") + .HasColumnType("numeric"); + + b.Property("WomenShortCutPrice") + .HasColumnType("numeric"); + + b.HasKey("UserId"); + + b.HasIndex("ScheduleOwnerId"); + + b.ToTable("BrusherProfile"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairCutQuery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("AdditionalInfo") + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Consent") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("PaymentId") + .HasColumnType("text"); + + b.Property("PerformerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.Property("Provisional") + .HasColumnType("numeric"); + + b.Property("SelectedProfileUserId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.Property("ValidationDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.HasIndex("ClientId"); + + b.HasIndex("LocationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PerformerId"); + + b.HasIndex("PrestationId"); + + b.HasIndex("SelectedProfileUserId"); + + b.ToTable("HairCutQueries"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Consent") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("PaymentId") + .HasColumnType("text"); + + b.Property("PerformerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provisional") + .HasColumnType("numeric"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.Property("ValidationDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.HasIndex("ClientId"); + + b.HasIndex("LocationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PerformerId"); + + b.ToTable("HairMultiCutQueries"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Cares") + .HasColumnType("boolean"); + + b.Property("Cut") + .HasColumnType("boolean"); + + b.Property("Dressing") + .HasColumnType("integer"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("Length") + .HasColumnType("integer"); + + b.Property("Shampoo") + .HasColumnType("boolean"); + + b.Property("Tech") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("HairPrestation"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestationCollectionItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.Property("QueryId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PrestationId"); + + b.HasIndex("QueryId"); + + b.ToTable("HairPrestationCollectionItem"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Brand") + .HasColumnType("text"); + + b.Property("ColorId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ColorId"); + + b.ToTable("HairTaint"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaintInstance", b => + { + b.Property("TaintId") + .HasColumnType("bigint"); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.HasKey("TaintId", "PrestationId"); + + b.HasIndex("PrestationId"); + + b.ToTable("HairTaintInstance"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Evolution.Feature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ShortName") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Feature"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Fixing.Bug", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasMaxLength(10240) + .HasColumnType("character varying(10240)"); + + b.Property("FeatureId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.ToTable("Bug"); + }); + + modelBuilder.Entity("Yavsc.Models.Identity.DeviceDeclaration", b => + { + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("DeclarationDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("LOCALTIMESTAMP"); + + b.Property("DeviceOwnerId") + .HasColumnType("text"); + + b.Property("LatestActivityUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Model") + .HasColumnType("text"); + + b.Property("Platform") + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("DeviceId"); + + b.HasIndex("DeviceOwnerId"); + + b.ToTable("DeviceDeclaration"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.DeclarationFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DeclarationId") + .HasColumnType("bigint"); + + b.Property("MatchExcerpt") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PatternId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DeclarationId"); + + b.HasIndex("PatternId"); + + b.ToTable("DeclarationFlag"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.ModerationLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer"); + + b.Property("DeclarationId") + .HasColumnType("bigint"); + + b.Property("ModeratorId") + .HasColumnType("text"); + + b.Property("ScoreDelta") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DeclarationId"); + + b.HasIndex("ModeratorId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ModerationLogs", t => + { + t.HasCheckConstraint("CK_ModerationLog_Immutable", "1=1"); + }); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.RegexAlertPattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Pattern") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Severity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.ToTable("RegexAlertPatterns"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DeclarantTokenId") + .HasColumnType("uuid"); + + b.Property("ScoreDelta") + .HasColumnType("integer"); + + b.Property("Sentiment") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrustTokenId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("SubmittedAt"); + + b.HasIndex("TrustTokenId"); + + b.ToTable("TrustDeclarations"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TokenSource") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("TrustScore") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.ToTable("TrustTokens"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Depth") + .HasColumnType("numeric"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("numeric"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.Property("Weight") + .HasColumnType("numeric"); + + b.Property("Width") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContextId") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ContextId"); + + b.ToTable("Services"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.Announce", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("For") + .HasColumnType("smallint"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Sender") + .HasColumnType("text"); + + b.Property("Topic") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Announce"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.DismissClicked", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("NotificationId") + .HasColumnType("bigint"); + + b.HasKey("UserId", "NotificationId"); + + b.HasIndex("NotificationId"); + + b.ToTable("DismissClicked"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Instrument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("Instrument"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.InstrumentRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("InstrumentId", "OwnerId"); + + b.HasIndex("OwnerId"); + + b.ToTable("InstrumentRating"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalPreference", b => + { + b.Property("OwnerProfileId") + .HasColumnType("text"); + + b.Property("DjSettingsUserId") + .HasColumnType("text"); + + b.Property("MusicLoverSettingsUserId") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("TendencyId") + .HasColumnType("bigint"); + + b.HasKey("OwnerProfileId"); + + b.HasIndex("DjSettingsUserId"); + + b.HasIndex("MusicLoverSettingsUserId"); + + b.HasIndex("TendencyId"); + + b.ToTable("MusicalPreference"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalTendency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("MusicalTendency"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.DjSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SoundCloudId") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("DjSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.Instrumentation", b => + { + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("InstrumentId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("Instrumentation"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("MusicLoverSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.Property("CreationToken") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ExecutorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderReference") + .HasColumnType("text"); + + b.Property("PaypalPayerId") + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("CreationToken"); + + b.HasIndex("ExecutorId"); + + b.ToTable("PayPalPayment"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApplicationUserId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Circle"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.CircleMember", b => + { + b.Property("MemberId") + .HasColumnType("text"); + + b.Property("CircleId") + .HasColumnType("bigint"); + + b.HasKey("MemberId", "CircleId"); + + b.HasIndex("CircleId"); + + b.ToTable("CircleMembers"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Contact", b => + { + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("ApplicationUserId") + .HasColumnType("text"); + + b.Property("EMail") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("OwnerId", "UserId"); + + b.HasIndex("AddressId"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Contact"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.HyperLink", b => + { + b.Property("HRef") + .HasColumnType("text"); + + b.Property("Method") + .HasColumnType("text"); + + b.Property("BrusherProfileUserId") + .HasColumnType("text"); + + b.Property("ContentType") + .HasColumnType("text"); + + b.Property("PayPalPaymentCreationToken") + .HasColumnType("text"); + + b.Property("Rel") + .HasColumnType("text"); + + b.HasKey("HRef", "Method"); + + b.HasIndex("BrusherProfileUserId"); + + b.HasIndex("PayPalPaymentCreationToken"); + + b.ToTable("HyperLink"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.PostalAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("text"); + + b.Property("Street1") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("Yavsc.Models.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("SiteSkills"); + }); + + modelBuilder.Entity("Yavsc.Models.Streaming.LiveFlow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DifferedFileName") + .HasColumnType("text"); + + b.Property("MediaType") + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Pitch") + .HasColumnType("text"); + + b.Property("SequenceNumber") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("LiveFlow"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.Property("Code") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Hidden") + .HasColumnType("boolean"); + + b.Property("Moderated") + .HasColumnType("boolean"); + + b.Property("ModeratorGroupName") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCode") + .HasColumnType("text"); + + b.Property("Photo") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("SettingsClassName") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Code"); + + b.HasIndex("ParentCode"); + + b.ToTable("Activities"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CoWorking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FormationSettingsUserId") + .HasColumnType("text"); + + b.Property("PerformerId") + .HasColumnType("text"); + + b.Property("WorkingForId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FormationSettingsUserId"); + + b.HasIndex("PerformerId"); + + b.HasIndex("WorkingForId"); + + b.ToTable("CoWorking"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CommandForm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionName") + .HasColumnType("text"); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.ToTable("CommandForm"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.Property("PerformerId") + .HasColumnType("text"); + + b.Property("AcceptNotifications") + .HasColumnType("boolean"); + + b.Property("AcceptPublicContact") + .HasColumnType("boolean"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("MaxDailyCost") + .HasColumnType("integer"); + + b.Property("MinDailyCost") + .HasColumnType("integer"); + + b.Property("OrganizationAddressId") + .HasColumnType("bigint"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("SIREN") + .IsRequired() + .HasColumnType("text"); + + b.Property("UseGeoLocalizationToReduceDistanceWithClients") + .HasColumnType("boolean"); + + b.Property("WebSite") + .HasColumnType("text"); + + b.HasKey("PerformerId"); + + b.HasIndex("OrganizationAddressId"); + + b.ToTable("Performers"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Profiles.FormationSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("FormationSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.RdvQuery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Consent") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("LocationType") + .HasColumnType("integer"); + + b.Property("PaymentId") + .HasColumnType("text"); + + b.Property("PerformerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provisional") + .HasColumnType("numeric"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.Property("ValidationDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.HasIndex("ClientId"); + + b.HasIndex("LocationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PerformerId"); + + b.ToTable("RdvQueries"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.UserActivity", b => + { + b.Property("DoesCode") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Weight") + .HasColumnType("integer"); + + b.HasKey("DoesCode", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("UserActivities"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.Calendar.Period", b => + { + b.Property("Start") + .HasColumnType("timestamp with time zone"); + + b.Property("End") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Start", "End"); + + b.ToTable("Period"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.EMailing.MailingTemplate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(65536) + .HasColumnType("character varying(65536)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ReplyToAddress") + .HasColumnType("text"); + + b.Property("ToSend") + .HasColumnType("integer"); + + b.Property("Topic") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("MailingTemplate"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Consent") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("GitId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("text"); + + b.Property("PerformerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provisional") + .HasColumnType("numeric"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.Property("ValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.HasIndex("ClientId"); + + b.HasIndex("GitId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PerformerId"); + + b.ToTable("Project"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.ProjectBuildConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProjectBuildConfiguration"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Branch") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("GitRepositoryReference"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("UserClaims") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Properties") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceScope", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Scopes") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceSecret", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Secrets") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", null) + .WithMany("UserClaims") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", "Scope") + .WithMany() + .HasForeignKey("ScopeId1"); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", null) + .WithMany("Properties") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", "Scope") + .WithMany() + .HasForeignKey("ScopeId1"); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("Claims") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("AllowedCorsOrigins") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("AllowedGrantTypes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("IdentityProviderRestrictions") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("PostLogoutRedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("Properties") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("RedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("AllowedScopes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientSecret", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("ClientSecrets") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.IdentityResource", "IdentityResource") + .WithMany("UserClaims") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.IdentityResource", "IdentityResource") + .WithMany("Properties") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityResource"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Yavsc.Models.Access.Ban", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "TargetUser") + .WithMany() + .HasForeignKey("TargetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TargetUser"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.BlackListed", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("BlackList") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToBlogPost", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Target") + .WithMany("ACL") + .HasForeignKey("BlogPostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Circle", "Allowed") + .WithMany() + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Allowed"); + + b.Navigation("Target"); + }); + + modelBuilder.Entity("Yavsc.Models.AccountBalance", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithOne("AccountBalance") + .HasForeignKey("Yavsc.Models.AccountBalance", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "PostalAddress") + .WithMany() + .HasForeignKey("PostalAddressId"); + + b.Navigation("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.BalanceImpact", b => + { + b.HasOne("Yavsc.Models.AccountBalance", "Balance") + .WithMany() + .HasForeignKey("BalanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Balance"); + }); + + modelBuilder.Entity("Yavsc.Models.Bank.BankIdentity", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany("BankInfo") + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.CommandLine", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", null) + .WithMany("Bill") + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Billing.EstimateTemplate", null) + .WithMany("Bill") + .HasForeignKey("EstimateTemplateId"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.RdvQuery", "Query") + .WithMany() + .HasForeignKey("CommandId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Client"); + + b.Navigation("Owner"); + + b.Navigation("Query"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Signature", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", "Estimate") + .WithMany("Signatures") + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Signer") + .WithMany() + .HasForeignKey("SignerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Estimate"); + + b.Navigation("Signer"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b => + { + b.HasOne("Yavsc.Models.Blog.UploadedFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Author") + .WithMany("Posts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogTag", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany("Tags") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Author") + .WithMany("BlogComments") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.Comment", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId"); + + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany("Comments") + .HasForeignKey("ReceiverId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Parent"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "BlogPost") + .WithMany() + .HasForeignKey("BlogpostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BlogPost"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.ScheduledEvent", b => + { + b.HasOne("Yavsc.Models.Calendar.Schedule", null) + .WithMany("Events") + .HasForeignKey("ScheduleOwnerId"); + + b.HasOne("Yavsc.Server.Models.Calendar.Period", "Period") + .WithMany() + .HasForeignKey("PeriodStart", "PeriodEnd"); + + b.Navigation("Period"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatConnection", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("Connections") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("Rooms") + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoomAccess", b => + { + b.HasOne("Yavsc.Models.Chat.ChatRoom", "Room") + .WithMany("Moderation") + .HasForeignKey("ChannelName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany("RoomAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.HasOne("Yavsc.Models.Calendar.Schedule", "Schedule") + .WithMany() + .HasForeignKey("ScheduleOwnerId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "BaseProfile") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseProfile"); + + b.Navigation("Schedule"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairCutQuery", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany() + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId"); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization") + .WithMany() + .HasForeignKey("PaymentId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "PerformerProfile") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany() + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.BrusherProfile", "SelectedProfile") + .WithMany() + .HasForeignKey("SelectedProfileUserId"); + + b.Navigation("Client"); + + b.Navigation("Context"); + + b.Navigation("Location"); + + b.Navigation("PerformerProfile"); + + b.Navigation("Prestation"); + + b.Navigation("Regularization"); + + b.Navigation("SelectedProfile"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany() + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId"); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization") + .WithMany() + .HasForeignKey("PaymentId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "PerformerProfile") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Context"); + + b.Navigation("Location"); + + b.Navigation("PerformerProfile"); + + b.Navigation("Regularization"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestationCollectionItem", b => + { + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany() + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairMultiCutQuery", "Query") + .WithMany("Prestations") + .HasForeignKey("QueryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prestation"); + + b.Navigation("Query"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaint", b => + { + b.HasOne("Yavsc.Models.Drawing.Color", "Color") + .WithMany() + .HasForeignKey("ColorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Color"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaintInstance", b => + { + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany("Taints") + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairTaint", "Taint") + .WithMany() + .HasForeignKey("TaintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prestation"); + + b.Navigation("Taint"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Fixing.Bug", b => + { + b.HasOne("Yavsc.Models.IT.Evolution.Feature", "False") + .WithMany() + .HasForeignKey("FeatureId"); + + b.Navigation("False"); + }); + + modelBuilder.Entity("Yavsc.Models.Identity.DeviceDeclaration", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "DeviceOwner") + .WithMany("DeviceDeclaration") + .HasForeignKey("DeviceOwnerId"); + + b.Navigation("DeviceOwner"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.DeclarationFlag", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustDeclaration", "Declaration") + .WithMany("Flags") + .HasForeignKey("DeclarationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Kyc.RegexAlertPattern", "Pattern") + .WithMany() + .HasForeignKey("PatternId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Declaration"); + + b.Navigation("Pattern"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.ModerationLog", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustDeclaration", "Declaration") + .WithMany() + .HasForeignKey("DeclarationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Declaration"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustToken", "Subject") + .WithMany("Declarations") + .HasForeignKey("TrustTokenId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Subject"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Service", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany("Services") + .HasForeignKey("ContextId"); + + b.Navigation("Context"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.Announce", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.DismissClicked", b => + { + b.HasOne("Yavsc.Abstract.Models.Messaging.Notification", "Notified") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notified"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.InstrumentRating", b => + { + b.HasOne("Yavsc.Models.Musical.Instrument", "Instrument") + .WithMany() + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Profile") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instrument"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalPreference", b => + { + b.HasOne("Yavsc.Models.Musical.Profiles.DjSettings", null) + .WithMany("SoundColor") + .HasForeignKey("DjSettingsUserId"); + + b.HasOne("Yavsc.Models.Musical.Profiles.MusicLoverSettings", null) + .WithMany("SoundColor") + .HasForeignKey("MusicLoverSettingsUserId"); + + b.HasOne("Yavsc.Models.Musical.MusicalTendency", "MusicalTendency") + .WithMany() + .HasForeignKey("TendencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MusicalTendency"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.Instrumentation", b => + { + b.HasOne("Yavsc.Models.Musical.Instrument", "Tool") + .WithMany() + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tool"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Executor") + .WithMany() + .HasForeignKey("ExecutorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Executor"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany("Circles") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.CircleMember", b => + { + b.HasOne("Yavsc.Models.Relationship.Circle", "Circle") + .WithMany("Members") + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Member") + .WithMany("Membership") + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Circle"); + + b.Navigation("Member"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Contact", b => + { + b.HasOne("Yavsc.Models.Relationship.PostalAddress", "PostalAddress") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany("Book") + .HasForeignKey("ApplicationUserId"); + + b.Navigation("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.HyperLink", b => + { + b.HasOne("Yavsc.Models.Haircut.BrusherProfile", null) + .WithMany("Links") + .HasForeignKey("BrusherProfileUserId"); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", null) + .WithMany("Links") + .HasForeignKey("PayPalPaymentCreationToken"); + }); + + modelBuilder.Entity("Yavsc.Models.Streaming.LiveFlow", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Parent") + .WithMany("Children") + .HasForeignKey("ParentCode"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CoWorking", b => + { + b.HasOne("Yavsc.Models.Workflow.Profiles.FormationSettings", null) + .WithMany("CoWorking") + .HasForeignKey("FormationSettingsUserId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Performer") + .WithMany() + .HasForeignKey("PerformerId"); + + b.HasOne("Yavsc.Models.ApplicationUser", "WorkingFor") + .WithMany() + .HasForeignKey("WorkingForId"); + + b.Navigation("Performer"); + + b.Navigation("WorkingFor"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CommandForm", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany("Forms") + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Context"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "OrganizationAddress") + .WithMany() + .HasForeignKey("OrganizationAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Performer") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationAddress"); + + b.Navigation("Performer"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.RdvQuery", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany() + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId"); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization") + .WithMany() + .HasForeignKey("PaymentId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "PerformerProfile") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Context"); + + b.Navigation("Location"); + + b.Navigation("PerformerProfile"); + + b.Navigation("Regularization"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.UserActivity", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Does") + .WithMany() + .HasForeignKey("DoesCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "User") + .WithMany("Activity") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Does"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany() + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", "Repository") + .WithMany() + .HasForeignKey("GitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization") + .WithMany() + .HasForeignKey("PaymentId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "PerformerProfile") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Context"); + + b.Navigation("PerformerProfile"); + + b.Navigation("Regularization"); + + b.Navigation("Repository"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.ProjectBuildConfiguration", b => + { + b.HasOne("Yavsc.Server.Models.IT.Project", "TargetProject") + .WithMany("Configurations") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TargetProject"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResource", b => + { + b.Navigation("Properties"); + + b.Navigation("Scopes"); + + b.Navigation("Secrets"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScope", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.Client", b => + { + b.Navigation("AllowedCorsOrigins"); + + b.Navigation("AllowedGrantTypes"); + + b.Navigation("AllowedScopes"); + + b.Navigation("Claims"); + + b.Navigation("ClientSecrets"); + + b.Navigation("IdentityProviderRestrictions"); + + b.Navigation("PostLogoutRedirectUris"); + + b.Navigation("Properties"); + + b.Navigation("RedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResource", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.Navigation("AccountBalance"); + + b.Navigation("BankInfo"); + + b.Navigation("BlackList"); + + b.Navigation("BlogComments"); + + b.Navigation("Book"); + + b.Navigation("Circles"); + + b.Navigation("Connections"); + + b.Navigation("DeviceDeclaration"); + + b.Navigation("Membership"); + + b.Navigation("Posts"); + + b.Navigation("RoomAccess"); + + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.Navigation("Bill"); + + b.Navigation("Signatures"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b => + { + b.Navigation("Bill"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.Navigation("ACL"); + + b.Navigation("Comments"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.Navigation("Children"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.Navigation("Moderation"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.Navigation("Prestations"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestation", b => + { + b.Navigation("Taints"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustToken", b => + { + b.Navigation("Declarations"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.DjSettings", b => + { + b.Navigation("SoundColor"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b => + { + b.Navigation("SoundColor"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.Navigation("Children"); + + b.Navigation("Forms"); + + b.Navigation("Services"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.Navigation("Activity"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Profiles.FormationSettings", b => + { + b.Navigation("CoWorking"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.Navigation("Configurations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Yavsc.Org/Migrations/20260820232152_DropCommentFromCircleAuthorizationToBlogPost.cs b/src/Yavsc.Org/Migrations/20260820232152_DropCommentFromCircleAuthorizationToBlogPost.cs new file mode 100644 index 00000000..f853889b --- /dev/null +++ b/src/Yavsc.Org/Migrations/20260820232152_DropCommentFromCircleAuthorizationToBlogPost.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Yavsc.Migrations +{ + /// + public partial class DropCommentFromCircleAuthorizationToBlogPost : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Comment", + table: "CircleAuthorizationToBlogPost"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Comment", + table: "CircleAuthorizationToBlogPost", + type: "boolean", + nullable: false, + defaultValue: false); + } + } +} diff --git a/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs index ef96638c..a0295ef6 100644 --- a/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs @@ -476,9 +476,6 @@ namespace Yavsc.Migrations b.Property("ClientId") .HasColumnType("integer"); - b.Property("ClientId1") - .HasColumnType("integer"); - b.Property("GrantType") .HasColumnType("text"); @@ -486,8 +483,6 @@ namespace Yavsc.Migrations b.HasIndex("ClientId"); - b.HasIndex("ClientId1"); - b.ToTable("ClientGrantTypes"); }); @@ -583,9 +578,6 @@ namespace Yavsc.Migrations b.Property("ClientId") .HasColumnType("integer"); - b.Property("ClientId1") - .HasColumnType("integer"); - b.Property("RedirectUri") .HasColumnType("text"); @@ -593,8 +585,6 @@ namespace Yavsc.Migrations b.HasIndex("ClientId"); - b.HasIndex("ClientId1"); - b.ToTable("ClientRedirectUris"); }); @@ -609,9 +599,6 @@ namespace Yavsc.Migrations b.Property("ClientId") .HasColumnType("integer"); - b.Property("ClientId1") - .HasColumnType("integer"); - b.Property("Scope") .HasColumnType("text"); @@ -619,8 +606,6 @@ namespace Yavsc.Migrations b.HasIndex("ClientId"); - b.HasIndex("ClientId1"); - b.ToTable("ClientScopes"); }); @@ -1096,9 +1081,6 @@ namespace Yavsc.Migrations b.Property("BlogPostId") .HasColumnType("bigint"); - b.Property("Comment") - .HasColumnType("boolean"); - b.HasKey("CircleId", "BlogPostId"); b.HasIndex("BlogPostId"); @@ -3460,16 +3442,12 @@ namespace Yavsc.Migrations modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b => { - b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") .WithMany("AllowedGrantTypes") .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") - .WithMany() - .HasForeignKey("ClientId1"); - b.Navigation("Client"); }); @@ -3520,31 +3498,23 @@ namespace Yavsc.Migrations modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b => { - b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") .WithMany("RedirectUris") .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") - .WithMany() - .HasForeignKey("ClientId1"); - b.Navigation("Client"); }); modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b => { - b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") .WithMany("AllowedScopes") .HasForeignKey("ClientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") - .WithMany() - .HasForeignKey("ClientId1"); - b.Navigation("Client"); }); diff --git a/src/Yavsc.Org/ViewModels/Manage/SetUserNameViewModel.cs b/src/Yavsc.Org/ViewModels/Manage/SetUserNameViewModel.cs index 69507b92..61cf0727 100644 --- a/src/Yavsc.Org/ViewModels/Manage/SetUserNameViewModel.cs +++ b/src/Yavsc.Org/ViewModels/Manage/SetUserNameViewModel.cs @@ -7,7 +7,7 @@ namespace Yavsc.ViewModels.Manage public class SetUserNameViewModel { [Required] - [Display(Name = "User name"),RegularExpression(YavscConstants.UserNameRegExp)] + [Display(Name = "User name"),RegularExpression(Constants.UserNameRegExp)] public string UserName { get; set; } } diff --git a/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml b/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml index f0a961c9..dd6b0ff8 100644 --- a/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml +++ b/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml @@ -13,7 +13,7 @@ } else {
Utilisateur inconnu - Utilisateur inconnu + Utilisateur inconnu
} diff --git a/src/Yavsc.Org/Views/Shared/_LoginPartial.cshtml b/src/Yavsc.Org/Views/Shared/_LoginPartial.cshtml index 431fe61e..7bfa2a3f 100644 --- a/src/Yavsc.Org/Views/Shared/_LoginPartial.cshtml +++ b/src/Yavsc.Org/Views/Shared/_LoginPartial.cshtml @@ -16,7 +16,7 @@
  • Features
  • - @if (User.IsInMsRole(YavscConstants.AdminGroupName)) { + @if (User.IsInMsRole(Constants.AdminGroupName)) {