diff --git a/.dockerignore b/.dockerignore
index 45b7c22b..444995d0 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,6 +1,7 @@
**/bin/
**/obj/
**/.playwright/
+.git/
.vs/
.github/
# Exclure uniquement les dossiers de sortie de compilation
@@ -13,4 +14,5 @@ test/*/obj/
# Exclure les caches lourds
**/.playwright/
+.git/
.vs/
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 48d3b4d1..ae9780df 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -59,7 +59,7 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
- uses: actions/checkout@v7
+ uses: actions/checkout@v4
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 76dc08d5..c374bc6b 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -14,7 +14,7 @@
"name": "Yavsc.Org",
"type": "dotnet",
"request": "launch",
- "projectPath": "${workspaceFolder}/src/Yavsc.Org/Yavsc.Org.csproj",
+ "projectPath": "${workspaceFolder}/src/Yavsc.Org/Yavsc.Org.csproj"
},
{
"name": "Yavsc.Blogs",
diff --git a/.vscode/mcp.json b/.vscode/mcp.json
deleted file mode 100644
index 7ca6ed4b..00000000
--- a/.vscode/mcp.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "servers": {
- "openclaw": {
- "type": "stdio",
- "command": "/home/paul/.nvm/versions/node/v22.23.0/bin/node",
- "args": [
- "/home/paul/Workspace/tools/openclaw-mcp-server.js"
- ]
- }
- }
-}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 6e22cb5e..0a4785b9 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -26,17 +26,5 @@
"cSpell.language": "fr,en",
"makefile.configureOnOpen": false,
"search.useGlobalIgnoreFiles": true,
- "search.useParentIgnoreFiles": true,
- "chat.mcp.serverSampling": {
- "yavsc/.vscode/mcp.json: openclaw": {
- "allowedModels": [
- "copilot/auto",
- "copilotcli/claude-haiku-4.5",
- "copilotcli/gpt-4.1",
- "copilotcli/gpt-5-mini",
- "copilotcli/mai-code-1-flash-picker",
- "copilotcli/gpt-5.3-codex"
- ]
- }
- }
+ "search.useParentIgnoreFiles": true
}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 021c5e53..b1ed6926 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -2,11 +2,20 @@
true
+
-
-
-
+
+
+
@@ -15,7 +24,7 @@
-
+
diff --git a/Dockerfile b/Dockerfile
index 88b11683..e534f8ad 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -48,7 +48,7 @@ COPY . .
# (3) Source NuGet interne (Letsencrypt, certificat auto-signé côté
# serveur, justifié par build privé).
-RUN dotnet nuget add source https://isn.pschneider.fr/api/v3/index.json --allow-insecure-connections
+RUN dotnet nuget add source https://isn.pschneider.fr/v3/index.json --allow-insecure-connections
# (4) Restore
RUN dotnet restore
diff --git a/Dockerfile.backend b/Dockerfile.backend
index 76ad9ea0..86df9ccd 100644
--- a/Dockerfile.backend
+++ b/Dockerfile.backend
@@ -26,7 +26,7 @@ COPY src/PostIt/PostIt.Desktop/*.csproj ./src/PostIt/PostIt.Desktop/
COPY . .
# 3. Restauration des dépendances avec vos workloads actifs
-RUN dotnet nuget add source https://isn.pschneider.fr/api/v3/index.json
+RUN dotnet nuget add source https://isn.pschneider.fr/v3/index.json --allow-insecure-connections
# 4. Restauration des dépendances pour tous les projets
RUN dotnet restore
diff --git a/Makefile b/Makefile
index 2683c542..a4922a3d 100644
--- a/Makefile
+++ b/Makefile
@@ -10,8 +10,8 @@ include .env
all:
dotnet build --nologo
-clean:
- dotnet clean -c $(CONFIG)
+clean:
+ dotnet clean
src/Yavsc/bin/output/wwwroot:
dotnet --project src/Yavsc.Org/Yavsc.Org.csproj publish
@@ -31,7 +31,7 @@ src/Yavsc.Server/bin/$(CONFIG)/$(FRAMEWORK)/Yavsc.Server.dll:
src/Yavsc/bin/$(CONFIG)/$(FRAMEWORK)/Yavsc.dll:
dotnet build -p:Configuration=$(CONFIG) --project src/Yavsc.Org/Yavsc.Org.csproj
-$(DESTDIR):
+$(DESTDIR):
mkdir $(DESTDIR)
install: $(DESTDIR)
diff --git a/README.md b/README.md
index d1ed912a..1f1e7ce5 100644
--- a/README.md
+++ b/README.md
@@ -152,13 +152,6 @@ d'abord `appsettings-org.json` du serveur ; sinon, laisse-le en place.
(utilisateur, mot de passe, hôte, base). Privilégier
`dotnet user-secrets` ou des variables d'environnement `ASPNETCORE_*`
plutôt qu'un mot de passe en clair dans le fichier.
-- Au démarrage, Yavsc.Org applique automatiquement ses migrations EF
- Core. Sur cette base de code, EF Core 10 peut encore lever un
- `PendingModelChangesWarning` malgré des migrations et snapshots déjà
- alignés ; ce faux positif est ignoré sur les contextes PostgreSQL pour
- éviter un démarrage inutilement en mode dégradé. Si une erreur de
- migration apparaît encore en production, elle doit être traitée comme
- une vraie divergence de schéma ou de connexion.
- `Smtp.*` — hôte, port, identifiants SMTP pour l'envoi d'e-mails
transactionnels.
- `Authentication.PayPal.*` et `Authentication.Google.*` — clés d'API
diff --git a/doc/dev-tracking/client-editor-overhaul.md b/doc/dev-tracking/client-editor-overhaul.md
new file mode 100644
index 00000000..0231e6aa
--- /dev/null
+++ b/doc/dev-tracking/client-editor-overhaul.md
@@ -0,0 +1,278 @@
+# Client editor overhaul — Yavsc.Org administration
+
+## Goal
+
+Bring the OAuth2 client administration UI (`/Client/Edit/{id}` and friends)
+in Yavsc.Org to feature parity with the IdentityServer8 `Client` entity
+model. Today the editor only exposes a handful of scalar fields and a few
+single-line inputs for collections; the bulk of the entity and its
+related collections are unreachable from the UI.
+
+## Inventory — current state
+
+### Properties exposed by `Views/Client/Edit.cshtml`
+
+| Field | Type | Notes |
+| ------------------------ | ----------- | ---------------------------------- |
+| `ClientId` | string | hidden, identifier |
+| `Enabled` | bool | checkbox |
+| `ClientName` | string | display name |
+| `FrontChannelLogoutUri` | string | only front-channel, no back-channel |
+| `RedirectUris` | collection | rendered as a single text input |
+| `IdentityTokenLifetime` | int | seconds |
+| `AbsoluteRefreshTokenLifetime` | int | seconds |
+| `ClientSecrets` | collection | rendered as a single text input |
+| `AccessTokenType` | enum | dropdown (custom `SetAppTypesInputValues`) |
+
+### Properties of `IdentityServer8.EntityFramework.Entities.Client` **NOT** in the editor
+
+Core scalars (16 fields missing):
+
+- `Description`
+- `ClientUri`
+- `LogoUri`
+- `RequireConsent`
+- `RequirePkce`
+- `RequireRequestObject`
+- `RequireClientSecret`
+- `AllowPlainTextPkce`
+- `AllowOfflineAccess`
+- `AllowRememberConsent`
+- `AlwaysIncludeUserClaimsInIdToken`
+- `AlwaysSendClientClaims`
+- `AuthorizationCodeLifetime`
+- `BackChannelLogoutUri`
+- `BackChannelLogoutSessionRequired`
+- `CibaLifetime`
+- `ClientClaimsPrefix`
+- `ConsentLifetime`
+- `Created`
+- `DeviceCodeLifetime`
+- `EnableLocalLogin`
+- `Enabled`
+- `FrontChannelLogoutSessionRequired`
+- `IncludeJwtId`
+- `LastAccessed`
+- `LogoUri`
+- `NonEditable`
+- `PairwiseSubjectSalt`
+- `PollingInterval`
+- `ProtocolType`
+- `RefreshTokenExpiration`
+- `RefreshTokenUsage`
+- `SlidingRefreshTokenLifetime`
+- `UpdateAccessTokenClaimsOnRefresh`
+- `Updated`
+- `UserCodeType`
+- `UserSsoLifetime`
+
+Collections (8 missing — currently either not exposed at all, or jammed
+into a single-line text input that doesn't work for an IEnumerable):
+
+- `AllowedGrantTypes` → `ClientGrantType` (GrantType)
+- `AllowedScopes` → `ClientScope` (Scope)
+- `RedirectUris` → `ClientRedirectUri` (RedirectUri) — exposed but broken
+- `PostLogoutRedirectUris` → `ClientPostLogoutRedirectUri` (PostLogoutRedirectUri)
+- `AllowedCorsOrigins` → `ClientCorsOrigin` (Origin)
+- `IdentityProviderRestrictions` → `ClientIdPRestriction` (Provider)
+- `Claims` → `ClientClaim` (Type, Value)
+- `Properties` → `ClientProperty` (Key, Value)
+- `ClientSecrets` → `ClientSecret` (Type, Value, Description, Created, Expiration) — exposed but broken
+- `AllowedSigningAlgorithms` → scalar string collection on Client itself
+
+## Pages to add
+
+Pattern: one Razor page per collection under
+`Views/Client/Edit{Collection}.cshtml`. Each page lists existing rows,
+offers an "Add" form with the relevant fields, and a per-row
+remove button. The main `Edit.cshtml` becomes a hub page with links
+to each subpage plus the scalar fields it already has.
+
+| Page | Route | Form fields |
+| ------------------------------------- | ------------------------------------------ | ------------------------------------------------- |
+| `Edit.cshtml` | `GET /Client/Edit/{id}` (existing) | scalar fields + nav links |
+| `EditRedirectUris.cshtml` | `GET /Client/EditRedirectUris/{id}` | `RedirectUri` |
+| `EditPostLogoutRedirectUris.cshtml` | `GET /Client/EditPostLogoutRedirectUris/{id}` | `PostLogoutRedirectUri` |
+| `EditScopes.cshtml` | `GET /Client/EditScopes/{id}` | `Scope` (with select of known scopes) |
+| `EditGrantTypes.cshtml` | `GET /Client/EditGrantTypes/{id}` | `GrantType` (with select of known types) |
+| `EditCorsOrigins.cshtml` | `GET /Client/EditCorsOrigins/{id}` | `Origin` |
+| `EditIdPRestrictions.cshtml` | `GET /Client/EditIdPRestrictions/{id}` | `Provider` |
+| `EditClaims.cshtml` | `GET /Client/EditClaims/{id}` | `Type`, `Value` |
+| `EditProperties.cshtml` | `GET /Client/EditProperties/{id}` | `Key`, `Value` |
+| `EditSecrets.cshtml` (replacement) | `GET /Client/EditSecrets/{id}` | `Type`, `Value`, `Description`, `Expiration` |
+
+Partial view `_EditableList.cshtml` factored once and consumed by all
+of the above.
+
+## Controller actions to add
+
+For each collection `Foo`:
+
+- `GET EditFoo(int id)` — load the client, render the page
+- `POST AddFoo(int id, …)` — append a row, redirect to `EditFoo`
+- `POST RemoveFoo(int id, int rowId)` — delete a row, redirect
+
+## Verification
+
+- `dotnet build src/Yavsc.Org/Yavsc.Org.csproj` → 0 errors
+- No tests in `Yavsc.Org.Tests` exercise the controller today (per
+ `find … -name "ClientController*" -not -path "*/bin/*"`). Smoke-test
+ by logging in as admin, hitting `/Client/Edit/1`, then each
+ `Edit*/1` page, and verifying the add/remove POSTs.
+- Existing seed flow (`MigratePostItClientToPublic` in
+ `HostingExtensions.cs`) must keep working — the editor changes are
+ additive, not destructive.
+
+## Out of scope
+
+- Tests (no MVC test infrastructure currently exists for this controller)
+- Migration of existing collection fields (the broken `RedirectUris`
+ text input will simply be replaced by the new subpage)
+- Per-collection authorization policies (the controller is already
+ `[Authorize("AdministratorOnly")]`)
+- Client cloning / templating / JSON import-export
+
+## Status
+
+2026-06-21 16:04 — kickoff. Inventory done. Pages not yet started.
+
+2026-06-21 16:11 — first delivery, **build does not compile by design**
+(per Paul: "Tu peux même me laisser un travail qui ne compile
+pas"). The structural work is done; the residual errors are easy
+fixes Paul will do in a debug session.
+
+Files added (working tree, not yet committed):
+
+- `src/Yavsc.Org/Controllers/Administration/ClientController.Collections.cs`
+ — partial class with the per-collection GET / Add / Remove actions.
+- `src/Yavsc.Org/Views/Client/EditRedirectUris.cshtml`
+- `src/Yavsc.Org/Views/Client/EditPostLogoutRedirectUris.cshtml`
+- `src/Yavsc.Org/Views/Client/EditScopes.cshtml`
+- `src/Yavsc.Org/Views/Client/EditGrantTypes.cshtml`
+- `src/Yavsc.Org/Views/Client/EditCorsOrigins.cshtml`
+- `src/Yavsc.Org/Views/Client/EditIdPRestrictions.cshtml`
+- `src/Yavsc.Org/Views/Client/EditClaims.cshtml`
+- `src/Yavsc.Org/Views/Client/EditProperties.cshtml`
+- `src/Yavsc.Org/Views/Client/EditSecrets.cshtml`
+- `src/Yavsc.Org/Views/Client/_EditableStringList.cshtml`
+ — partial consumed by the single-string-field collection pages.
+
+Files modified:
+
+- `src/Yavsc.Org/Controllers/Administration/ClientController.cs`
+ — `class` → `partial class`; the `Edit(int id)` GET now uses
+ `LoadClientAsync` to load all navigations (so the new Edit.cshtml
+ can render counts in its nav links).
+- `src/Yavsc.Org/Views/Client/Edit.cshtml`
+ — significantly enriched: nav links to the 9 sub-pages, all the
+ scalar fields split into fieldsets (Security, Logout, Tokens,
+ Device / CIBA, Tokens-extra), ClientId / Id hidden.
+
+### Known residual compile errors (4 errors total)
+
+Paul is fixing these in a debug session. The structure is sound; the
+errors are missing properties on the `Client` entity, a Razor
+nullable quirk, and a `Localizer` injection miss.
+
+1. `Edit.cshtml:249` — `PairwiseSubjectSalt` doesn't exist on
+ `IdentityServer8.EntityFramework.Entities.Client`. **Fix**: drop
+ the field from Edit.cshtml; IdentityServer8 likely uses a
+ different property name (e.g. on a related entity) or doesn't
+ expose it.
+2. `Edit.cshtml:221` — `CibaLifetime` doesn't exist on `Client`.
+ **Fix**: same as above. CIBA flow may be configured elsewhere
+ (resource-level) or via a different property.
+3. `ClientController.Collections.cs` lines 181, 217, 253, 304 —
+ `Localizer` is not available in the partial class. **Fix**: inject
+ `IStringLocalizer` via the constructor, or
+ inline the strings ("BothTypeAndValueRequired", "KeyRequired",
+ "ValueRequired", "SecretValueRequired").
+4. `EditSecrets.cshtml:44` — `s.Expiration?.ToString("u")` on a
+ `DateTime?`. **Fix**: just `s.Expiration?.ToString("u")` works
+ if you write `s.Expiration.Value.ToString("u")`, or use
+ `(s.Expiration is null ? "" : s.Expiration.Value.ToString("u"))`,
+ or `s.Expiration?.ToString("u") ?? string.Empty`.
+
+### Suggested next session
+
+Once the 4 compile errors are fixed and the pages render:
+
+1. Smoke test by logging in as admin, hitting `/Client/Edit/1`,
+ then each `Edit*/1` page, and verifying add/remove POSTs.
+2. Add a confirmation prompt (or 2-step form) for Remove actions —
+ removing a Redirect URI is destructive and one click is too easy.
+3. Wire up some collection-level validation (e.g. redirect URI must
+ be a valid URL) at the controller level.
+4. Add tests — the project doesn't have MVC test infrastructure
+ today; consider adding a `Yavsc.Org.Tests` project that drives
+ the controller via `WebApplicationFactory`.
+
+
+## Test bootstrap notes (session of 2026-06-21 17:00+)
+
+When adding new integration tests against `WebServerFixture`:
+
+1. **Skip `/Account/Login` roundtrip.** The fixture ships without
+ `MapRazorPages()` (commented out in `HostingExtensions.ConfigurePipeline`),
+ so `/Identity/Account/Login` is 404, and the custom
+ `/Account/Login` route requires a complex antiforgery dance.
+ Instead, build a `ClaimsPrincipal` for the test user via
+ `UserManager` + `IUserClaimsPrincipalFactory`,
+ then call `IAuthenticationService.SignInAsync` on a synthetic
+ `DefaultHttpContext` and replay the resulting `Set-Cookie` header
+ into the test `HttpClient`. See
+ `ClientControllerCollectionTests.IssueIdentityCookie`.
+
+2. **Create the `Administrator` role before assigning it.** ASP.NET
+ Identity stores roles in `AspNetRoles`; there is no automatic seed.
+ The constant name is `YavscConstants.AdminGroupName` = `"Administrator"`.
+ Use `RoleManager.CreateAsync(new IdentityRole("Administrator"))`
+ before `AddToRoleAsync`.
+
+3. **Use `InMemory` connection string to bypass the prod signing-cert
+ requirement.** `HostingExtensions.AddIdentityServer` requires a
+ PEM cert unless `builder.Environment.IsDevelopment()` OR
+ `UsesInMemoryProvider(connectionString)`. The fixture already
+ uses `InMemory`, so `AddDeveloperSigningCredential()` is called
+ automatically — but only after we wired this check in (see
+ commit history).
+
+4. **Field-name gotchas** (from disassembling HigginsSoft
+ IdentityServer8.EntityFramework.Entities.Client 8.0.5-preview-net9):
+ - `PairWiseSubjectSalt` (capital W on "Wise"), not `PairwiseSubjectSalt`.
+ - `CibaLifetime` and `PollingInterval` do NOT exist on `Client` in
+ this version.
+ - `ConsentLifetime` and `UserSsoLifetime` are `int?`.
+
+5. **`MapStaticAssets()` fails on test projects.** Calling
+ `MapStaticAssets()` resolves a manifest file
+ (`.staticwebassets.endpoints.json`) that test projects
+ don't produce. Skip when `WebRootPath` points at the test
+ assembly directory.
+
+6. **Routing 404 on /Client/Edit/{id} via WebServerFixture.** As of
+ this session, the GET endpoint returns 404 even with admin
+ header. The route mapping is intact
+ (`MapDefaultControllerRoute()`), so this is likely an MVC
+ convention routing issue with the
+ `Controllers/Administration/` subdirectory. To investigate
+ next session: log middleware pipeline or hit `/Client` index
+ first to see if any Client route resolves.
+
+7. **`MapStaticAssets()` is unconditional in prod, but blocks tests.**
+ `WebApplication.CreateBuilder` defaults `ContentRootPath` to
+ `AppContext.BaseDirectory`. In test runs that resolves to
+ `src/Yavsc.Org.Tests/bin/Debug/net10.0/`, where
+ `Yavsc.Org.Tests.staticwebassets.endpoints.json` doesn't exist
+ (it's generated only by projects with the Web SDK). The
+ `app.MapStaticAssets()` call inside `ConfigurePipeline` then
+ throws and the fixture fails to start — taking every test in
+ the `[Collection("Yavsc Server")]` down with it.
+
+ This is a pre-existing fragility of the WebServerFixture that
+ the new test work surfaced. Fixing it cleanly requires either:
+ (a) moving the test project to the Web SDK so it produces its
+ own manifest, (b) copying the manifest at build time via an
+ MSBuild target, or (c) routing `MapStaticAssets` through an
+ assembly-resolution fallback. None attempted in this session —
+ recorded for next session.
diff --git a/src/PostIt.Tests/FakeAuthorizingBrowser.cs b/src/PostIt.Tests/FakeAuthorizingBrowser.cs
index 4748425a..10311500 100644
--- a/src/PostIt.Tests/FakeAuthorizingBrowser.cs
+++ b/src/PostIt.Tests/FakeAuthorizingBrowser.cs
@@ -10,7 +10,7 @@ namespace PostIt.Tests;
/// URL emitted by OidcClient, extracts its state, and returns a
/// BrowserResult that mimics the OIDC redirect-with-code callback.
///
-/// The paired 's token endpoint accepts
+/// The paired 's token endpoint accepts
/// any authorization code, so we don't need to mint a real one here.
///
public sealed class FakeAuthorizingBrowser
diff --git a/src/PostIt.Tests/LoginPageViewModelTests.cs b/src/PostIt.Tests/LoginPageViewModelTests.cs
index 7a8b579f..e469c011 100644
--- a/src/PostIt.Tests/LoginPageViewModelTests.cs
+++ b/src/PostIt.Tests/LoginPageViewModelTests.cs
@@ -14,7 +14,7 @@ public class LoginPageViewModelTests
// short-circuits the system browser. The authority signs its
// access_token with RS256; the fake browser captures the redirect
// URI so the authority can complete the token exchange.
- using var authority = await OIDCStubAuthority.StartAsync();
+ using var authority = await OidcStubAuthority.StartAsync();
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
var settings = new PostIt.Settings
@@ -96,7 +96,7 @@ public class LoginPageViewModelTests
// double slash before /.well-known/openid-configuration. The
// stub advertises itself without the trailing slash; OidcClient
// must bridge.
- using var authority = await OIDCStubAuthority.StartAsync();
+ using var authority = await OidcStubAuthority.StartAsync();
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
var settings = new PostIt.Settings
@@ -254,4 +254,4 @@ public class LoginPageViewModelTests
"https://yavsc.example.com/.well-known/openid-configuration",
vm.StatusMessage);
}
-}
+}
\ No newline at end of file
diff --git a/src/PostIt.Tests/OidcStubAuthority.cs b/src/PostIt.Tests/OidcStubAuthority.cs
index 3c6552fb..552db6b0 100644
--- a/src/PostIt.Tests/OidcStubAuthority.cs
+++ b/src/PostIt.Tests/OidcStubAuthority.cs
@@ -20,7 +20,7 @@ namespace PostIt.Tests;
/// the browser intercepts the authorize redirect, the server completes
/// the token exchange.
///
-public sealed class OIDCStubAuthority : IAsyncDisposable, IDisposable
+public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable
{
private readonly HttpListener _listener;
private readonly RSA _rsa;
@@ -30,7 +30,7 @@ public sealed class OIDCStubAuthority : IAsyncDisposable, IDisposable
public string Issuer { get; }
public string LoopbackRedirectUri { get; }
- private OIDCStubAuthority(HttpListener listener, RSA rsa, string kid, string issuer, string loopback)
+ private OidcStubAuthority(HttpListener listener, RSA rsa, string kid, string issuer, string loopback)
{
_listener = listener;
_rsa = rsa;
@@ -39,7 +39,7 @@ public sealed class OIDCStubAuthority : IAsyncDisposable, IDisposable
LoopbackRedirectUri = loopback;
}
- public static async Task StartAsync()
+ public static async Task StartAsync()
{
// Pick a free loopback port.
var port = GetFreePort();
@@ -53,7 +53,7 @@ public sealed class OIDCStubAuthority : IAsyncDisposable, IDisposable
var rsa = RSA.Create(2048);
var kid = "test-key-1";
- var authority = new OIDCStubAuthority(listener, rsa, kid, prefix.TrimEnd('/'), loopback);
+ var authority = new OidcStubAuthority(listener, rsa, kid, prefix.TrimEnd('/'), loopback);
_ = Task.Run(() => authority.AcceptLoopAsync(authority._cts.Token));
return authority;
}
diff --git a/src/PostIt.Tests/SignaturePadControlTests.cs b/src/PostIt.Tests/SignaturePadControlTests.cs
deleted file mode 100644
index 691ae547..00000000
--- a/src/PostIt.Tests/SignaturePadControlTests.cs
+++ /dev/null
@@ -1,188 +0,0 @@
-using System;
-using System.Linq;
-using PostIt.Controls;
-using PostIt.Models;
-using Xunit;
-
-namespace PostIt.Tests;
-
-///
-/// Targeted tests for and
-/// .
-///
-/// The control exposes internal test hooks so we can drive
-/// the buffer without standing up a headless XAML tree just to
-/// deliver synthetic pointer events. The headless surface is used
-/// only to assert that the control's pointer handlers are wired
-/// when a template is applied; see
-/// .
-///
-public class SignaturePadControlTests
-{
- // --- SignaturePadData (pure) ---------------------------------------
-
- [Fact]
- public void Data_empty_array_is_empty()
- {
- var d = new SignaturePadData(Array.Empty());
- Assert.True(d.IsEmpty);
- Assert.Equal(0, d.StrokeCount);
- }
-
- [Fact]
- public void Data_single_dot_is_one_stroke_with_k_equals_one()
- {
- var d = new SignaturePadData(new[] { 1, 5_000, 5_000 });
- Assert.False(d.IsEmpty);
- Assert.Equal(1, d.StrokeCount);
- }
-
- [Fact]
- public void Data_two_strokes_are_independent()
- {
- var d = new SignaturePadData(new[]
- {
- 2, 100, 100, 200, 200,
- 1, 9_000, 9_000,
- });
- Assert.Equal(2, d.StrokeCount);
- }
-
- [Fact]
- public void Data_malformed_payload_does_not_throw_on_read()
- {
- // k=0 at the head would underflow the walker. The reader
- // short-circuits instead of throwing.
- var d = new SignaturePadData(new[] { 0, 1, 2, 3 });
- Assert.Equal(0, d.StrokeCount);
- }
-
- [Fact]
- public void Data_constructor_rejects_null()
- {
- Assert.Throws(() => new SignaturePadData(null!));
- }
-
- // --- SignaturePadControl (buffer / events) -------------------------
-
- [Fact]
- public void New_control_has_empty_buffer()
- {
- var pad = new SignaturePadControl();
- Assert.Empty(pad.Strokes);
- Assert.True(pad.Snapshot().IsEmpty);
- }
-
- [Fact]
- public void Snapshot_returns_a_distinct_array_each_call()
- {
- var pad = new SignaturePadControl();
- pad.AppendPointForTest(1_000, 2_000);
- pad.AppendPointForTest(3_000, 4_000);
- pad.SealStrokeForTest();
-
- var first = pad.Snapshot();
- var second = pad.Snapshot();
-
- // Distinct array instances — the consumer of the first
- // snapshot can hold onto it after the control mutates.
- Assert.NotSame(first.Strokes, second.Strokes);
- // Same logical content (no mutation in between).
- Assert.Equal(first.Strokes, second.Strokes);
-
- pad.AppendPointForTest(5_000, 6_000);
- pad.SealStrokeForTest();
-
- var third = pad.Snapshot();
- Assert.NotEqual(first.Strokes, third.Strokes);
- }
-
- [Fact]
- public void Clear_empties_buffer_and_raises_redraw()
- {
- var pad = new SignaturePadControl();
- pad.AppendPointForTest(1, 1);
- pad.SealStrokeForTest();
- Assert.NotEmpty(pad.Strokes);
-
- int redraws = 0;
- pad.RedrawRequested += (_, _) => redraws++;
- pad.Clear();
-
- Assert.Empty(pad.Strokes);
- Assert.True(pad.Snapshot().IsEmpty);
- Assert.Equal(1, redraws);
- }
-
- [Fact]
- public void SealStrokeForTest_raises_redraw()
- {
- var pad = new SignaturePadControl();
- int redraws = 0;
- pad.RedrawRequested += (_, _) => redraws++;
- pad.AppendPointForTest(1, 1);
- pad.AppendPointForTest(2, 2);
- pad.SealStrokeForTest();
- Assert.Equal(1, redraws);
- }
-
- [Fact]
- public void SealStrokeForTest_with_no_pending_points_is_a_no_op()
- {
- var pad = new SignaturePadControl();
- int redraws = 0;
- pad.RedrawRequested += (_, _) => redraws++;
- pad.SealStrokeForTest();
- Assert.Equal(0, redraws);
- }
-
- [Fact]
- public void Two_sealed_strokes_produce_two_length_prefixes()
- {
- var pad = new SignaturePadControl();
- // Stroke 0: one point.
- pad.AppendPointForTest(1_000, 1_000);
- pad.SealStrokeForTest();
- // Stroke 1: two points.
- pad.AppendPointForTest(2_000, 2_000);
- pad.AppendPointForTest(3_000, 3_000);
- pad.SealStrokeForTest();
-
- var s = pad.Strokes;
- // Layout: [k0, x0, y0, k1, x1, y1, x2, y2]
- Assert.Equal(1, s[0]);
- Assert.Equal(1_000, s[1]);
- Assert.Equal(1_000, s[2]);
- Assert.Equal(2, s[3]);
- Assert.Equal(2_000, s[4]);
- Assert.Equal(2_000, s[5]);
- Assert.Equal(3_000, s[6]);
- Assert.Equal(3_000, s[7]);
- }
-
- [Fact]
- public void StrokeCompleted_fires_on_seal()
- {
- var pad = new SignaturePadControl();
- int events = 0;
- pad.StrokeCompleted += (_, _) => events++;
- pad.AppendPointForTest(1, 1);
- pad.SealStrokeForTest();
- pad.AppendPointForTest(2, 2);
- pad.SealStrokeForTest();
- Assert.Equal(2, events);
- }
-
- [Fact]
- public void StrokeCompleted_carries_a_snapshot_with_k_count()
- {
- var pad = new SignaturePadControl();
- SignaturePadData? captured = null;
- pad.StrokeCompleted += (_, d) => captured = d;
- pad.AppendPointForTest(1, 1);
- pad.AppendPointForTest(2, 2);
- pad.SealStrokeForTest();
- Assert.NotNull(captured);
- Assert.Equal(1, captured!.StrokeCount);
- }
-}
diff --git a/src/PostIt.Tests/SignaturePageViewModelTests.cs b/src/PostIt.Tests/SignaturePageViewModelTests.cs
deleted file mode 100644
index 37f17a58..00000000
--- a/src/PostIt.Tests/SignaturePageViewModelTests.cs
+++ /dev/null
@@ -1,163 +0,0 @@
-using System;
-using System.IO;
-using System.Text.Json;
-using System.Threading.Tasks;
-using PostIt.Controls;
-using PostIt.ViewModels;
-using Xunit;
-
-namespace PostIt.Tests;
-
-///
-/// Tests for : the contract
-/// between the page's view model and the .
-/// The view (XAML + code-behind rendering) is not tested here — the
-/// control is render-agnostic, and the rendering is plain Polyline
-/// reconstruction that we'll exercise manually in PostIt.Desktop.
-///
-public class SignaturePageViewModelTests
-{
- [Fact]
- public void Default_constructor_uses_default_dimensions()
- {
- var vm = new SignaturePageViewModel();
- Assert.Equal(SignaturePageViewModel.DefaultWidth, vm.Width);
- Assert.Equal(SignaturePageViewModel.DefaultHeight, vm.Height);
- }
-
- [Fact]
- public void Constructor_rejects_non_positive_dimensions()
- {
- Assert.Throws(
- () => new SignaturePageViewModel(0, 100));
- Assert.Throws(
- () => new SignaturePageViewModel(100, 0));
- Assert.Throws(
- () => new SignaturePageViewModel(-1, 100));
- }
-
- [Fact]
- public void Attach_then_Detach_is_idempotent()
- {
- var vm = new SignaturePageViewModel();
- var pad = new SignaturePadControl();
- vm.Attach(pad);
- vm.Detach();
- // Second detach is a no-op: must not throw.
- vm.Detach();
- }
-
- [Fact]
- public void Attach_rejects_null()
- {
- var vm = new SignaturePageViewModel();
- Assert.Throws(() => vm.Attach(null!));
- }
-
- [Fact]
- public void StrokeCompleted_updates_status_and_counts()
- {
- var vm = new SignaturePageViewModel();
- var pad = new SignaturePadControl();
- vm.Attach(pad);
-
- // Drive the control via the test hooks so we don't depend
- // on Avalonia pointer events.
- pad.AppendPointForTest(1_000, 1_000);
- pad.AppendPointForTest(2_000, 2_000);
- pad.SealStrokeForTest();
-
- Assert.Equal(1, vm.StrokeCount);
- Assert.Equal(2, vm.PointCount);
- Assert.Contains("1 trait", vm.StatusMessage);
- }
-
- [Fact]
- public void Clear_resets_counts_and_buffer()
- {
- var vm = new SignaturePageViewModel();
- var pad = new SignaturePadControl();
- vm.Attach(pad);
-
- pad.AppendPointForTest(1, 1);
- pad.SealStrokeForTest();
- Assert.Equal(1, vm.StrokeCount);
-
- vm.Clear();
-
- Assert.Equal(0, vm.StrokeCount);
- Assert.Equal(0, vm.PointCount);
- Assert.Empty(pad.Strokes);
- Assert.Contains("Effacé", vm.StatusMessage);
- }
-
- [Fact]
- public async Task CaptureAsync_on_empty_buffer_reports_and_writes_nothing()
- {
- var vm = new SignaturePageViewModel();
- var pad = new SignaturePadControl();
- vm.Attach(pad);
-
- await vm.CaptureAsync();
-
- Assert.Contains("Rien", vm.StatusMessage);
- Assert.Null(vm.LastCapturedPath);
- }
-
- [Fact]
- public async Task CaptureAsync_writes_a_yavsc_signature_v1_file()
- {
- // The VM uses Environment.SpecialFolder.LocalApplicationData,
- // which we cannot redirect per-call without a constructor
- // seam. We test the produced file's structure rather than
- // its text formatting, because System.Text.Json's pretty-
- // printer is not part of the contract we're locking down.
- var vm = new SignaturePageViewModel();
- var pad = new SignaturePadControl();
- vm.Attach(pad);
-
- pad.AppendPointForTest(1_000, 2_000);
- pad.AppendPointForTest(3_000, 4_000);
- pad.SealStrokeForTest();
-
- await vm.CaptureAsync();
-
- Assert.NotNull(vm.LastCapturedPath);
- Assert.True(File.Exists(vm.LastCapturedPath!), $"file missing: {vm.LastCapturedPath}");
-
- using var doc = JsonDocument.Parse(File.ReadAllText(vm.LastCapturedPath!));
- var root = doc.RootElement;
-
- Assert.Equal("yavsc.signature/v1", root.GetProperty("format").GetString());
- Assert.Equal(10_000, root.GetProperty("coordinateMax").GetInt32());
- Assert.Equal(1, root.GetProperty("strokeCount").GetInt32());
-
- var strokes = root.GetProperty("strokes");
- Assert.Equal(JsonValueKind.Array, strokes.ValueKind);
- // [k=2, x0, y0, x1, y1]
- Assert.Equal(5, strokes.GetArrayLength());
- Assert.Equal(2, strokes[0].GetInt32()); // k (2 points)
- Assert.Equal(1_000, strokes[1].GetInt32()); // x0
- Assert.Equal(2_000, strokes[2].GetInt32()); // y0
- Assert.Equal(3_000, strokes[3].GetInt32()); // x1
- Assert.Equal(4_000, strokes[4].GetInt32()); // y1
- }
-
- [Fact]
- public async Task CaptureAsync_creates_directory_if_missing()
- {
- var vm = new SignaturePageViewModel();
- var pad = new SignaturePadControl();
- vm.Attach(pad);
- pad.AppendPointForTest(1, 1);
- pad.SealStrokeForTest();
-
- // The directory must exist after the call (CreateDirectory
- // in the VM handles this).
- await vm.CaptureAsync();
-
- var dir = Path.GetDirectoryName(vm.LastCapturedPath!);
- Assert.NotNull(dir);
- Assert.True(Directory.Exists(dir), $"directory missing: {dir}");
- }
-}
diff --git a/src/PostIt.Tests/YavscApiClientTests.cs b/src/PostIt.Tests/YavscApiClientTests.cs
index 03e2fa3f..1617c1b6 100644
--- a/src/PostIt.Tests/YavscApiClientTests.cs
+++ b/src/PostIt.Tests/YavscApiClientTests.cs
@@ -20,7 +20,7 @@ namespace PostIt.Tests;
/// End-to-end coverage of : silent
/// refresh on a near-expiry access token, 401-driven refresh + retry,
/// and persistence of the token bundle via .
-/// Uses the project's for the IdP and
+/// Uses the project's for the IdP and
/// a tiny in-process HTTP listener for the API server side.
///
public class YavscApiClientTests
@@ -45,7 +45,7 @@ public class YavscApiClientTests
// in-memory access token as expired and re-run a call. The
// refresh path must rotate the refresh token transparently
// and the API call must succeed with the new token.
- using var authority = await OIDCStubAuthority.StartAsync();
+ using var authority = await OidcStubAuthority.StartAsync();
using var apiServer = new StubApiServer();
await apiServer.StartAsync();
@@ -63,7 +63,7 @@ public class YavscApiClientTests
var reloaded = new YavscApiClient(settings, new TokenStore(tokensPath));
var posts = await reloaded.CallAsync>(
- HttpMethod.Get, "posts", TestContext.Current.CancellationToken);
+ HttpMethod.Get, "posts");
Assert.NotNull(posts);
Assert.NotEmpty(posts);
@@ -85,7 +85,7 @@ public class YavscApiClientTests
{
// API server returns 401 on the first request, 200 on the next.
// YavscApiClient must refresh, then retry exactly once.
- using var authority = await OIDCStubAuthority.StartAsync();
+ using var authority = await OidcStubAuthority.StartAsync();
using var apiServer = new StubApiServer(forceFirstRequest: true);
await apiServer.StartAsync();
@@ -97,7 +97,7 @@ public class YavscApiClientTests
settings, authority, tokensPath);
var posts = await client.CallAsync>(
- HttpMethod.Get, "posts", TestContext.Current.CancellationToken);
+ HttpMethod.Get, "posts");
Assert.NotEmpty(posts);
Assert.Equal(2, apiServer.RequestCount);
@@ -125,15 +125,14 @@ public class YavscApiClientTests
var client = new YavscApiClient(settings, new TokenStore(Path.Combine(
Path.GetTempPath(), $"postit-tests-noop-{Guid.NewGuid():N}.json")));
- await Assert.ThrowsAsync(
- () =>
- client.CallAsync(HttpMethod.Get, "posts", TestContext.Current.CancellationToken));
+ await Assert.ThrowsAsync(() =>
+ client.CallAsync(HttpMethod.Get, "posts"));
}
[Fact]
public async Task HasValidSession_is_true_after_login()
{
- using var authority = await OIDCStubAuthority.StartAsync();
+ using var authority = await OidcStubAuthority.StartAsync();
using var apiServer = new StubApiServer();
await apiServer.StartAsync();
@@ -155,7 +154,7 @@ public class YavscApiClientTests
// --- helpers --------------------------------------------------------
- private static PostIt.Settings BuildSettings(OIDCStubAuthority authority, string apiBaseUrl) => new()
+ private static PostIt.Settings BuildSettings(OidcStubAuthority authority, string apiBaseUrl) => new()
{
Authentication = new AuthenticationSettings
{
@@ -168,7 +167,7 @@ public class YavscApiClientTests
};
private static async Task LoginAndPersistAsync(
- PostIt.Settings settings, OIDCStubAuthority authority, string tokensPath)
+ PostIt.Settings settings, OidcStubAuthority authority, string tokensPath)
{
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
var client = new YavscApiClient(settings, new TokenStore(tokensPath));
@@ -182,7 +181,7 @@ public class YavscApiClientTests
///
/// YavscApiClient.LoginInteractiveAsync delegates to
/// Platform.CreateBrowser. We can't override that static cleanly
- /// from XUnit.v3, so we rebuild the call by re-routing the
+ /// from xunit.v3, so we rebuild the call by re-routing the
/// Platform.CreateBrowser delegate for the duration of the call.
///
private static async Task LoginWithBrowserAsync(
@@ -215,7 +214,7 @@ public class YavscApiClientTests
File.WriteAllText(tokensPath, JsonSerializer.Serialize(record));
}
- // --- OIDCLoginPhase progress tests ---------------------------------
+ // --- OidcLoginPhase progress tests ---------------------------------
///
/// Collecting Progress is documented to capture reports
@@ -226,7 +225,7 @@ public class YavscApiClientTests
[Fact]
public async Task LoginInteractiveAsync_reports_Discovering_then_Success()
{
- using var authority = await OIDCStubAuthority.StartAsync();
+ using var authority = await OidcStubAuthority.StartAsync();
using var apiServer = new StubApiServer();
await apiServer.StartAsync();
@@ -235,18 +234,18 @@ public class YavscApiClientTests
var client = new YavscApiClient(settings, new TokenStore(tokensPath));
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
- var reported = new System.Collections.Generic.List();
- var progress = new SyncProgress(reported);
+ var reported = new System.Collections.Generic.List();
+ var progress = new SyncProgress(reported);
try
{
await LoginWithBrowserAsync(client, browser.CreateBrowser(), progress);
// SyncProgress captures reports synchronously — no flush needed.
- Assert.Contains(OIDCLoginPhase.Discovering, reported);
- Assert.Contains(OIDCLoginPhase.OpeningBrowser, reported);
- Assert.Contains(OIDCLoginPhase.ExchangingCode, reported);
- Assert.Equal(OIDCLoginPhase.Success, Last(reported));
+ Assert.Contains(OidcLoginPhase.Discovering, reported);
+ Assert.Contains(OidcLoginPhase.OpeningBrowser, reported);
+ Assert.Contains(OidcLoginPhase.ExchangingCode, reported);
+ Assert.Equal(OidcLoginPhase.Success, Last(reported));
}
finally
{
@@ -257,24 +256,24 @@ public class YavscApiClientTests
[Fact]
public async Task LoginInteractiveAsync_reports_Error_when_browser_missing()
{
- using var authority = await OIDCStubAuthority.StartAsync();
+ using var authority = await OidcStubAuthority.StartAsync();
using var apiServer = new StubApiServer();
await apiServer.StartAsync();
var settings = BuildSettings(authority, apiServer.BaseUrl);
var client = new YavscApiClient(settings, new TokenStore(TokensPath()));
- var reported = new System.Collections.Generic.List();
- var progress = new SyncProgress(reported);
+ var reported = new System.Collections.Generic.List();
+ var progress = new SyncProgress(reported);
var original = Platform.CreateBrowser;
try
{
Platform.CreateBrowser = () => null; // simulate no browser wired up
await Assert.ThrowsAsync(
- () => client.LoginInteractiveAsync(progress, TestContext.Current.CancellationToken));
+ () => client.LoginInteractiveAsync(progress));
// SyncProgress captures reports synchronously — no flush needed.
- Assert.Equal(OIDCLoginPhase.Error, Last(reported));
+ Assert.Equal(OidcLoginPhase.Error, Last(reported));
}
finally
{
@@ -285,7 +284,7 @@ public class YavscApiClientTests
[Fact]
public async Task TrySilentLoginAsync_returns_false_when_no_bundle_on_disk()
{
- using var authority = await OIDCStubAuthority.StartAsync();
+ using var authority = await OidcStubAuthority.StartAsync();
using var apiServer = new StubApiServer();
await apiServer.StartAsync();
@@ -294,7 +293,7 @@ public class YavscApiClientTests
// Tokens file deliberately doesn't exist.
var client = new YavscApiClient(settings, new TokenStore(tokensPath));
- var ok = await client.TrySilentLoginAsync(null, TestContext.Current.CancellationToken);
+ var ok = await client.TrySilentLoginAsync();
Assert.False(ok);
Assert.False(client.HasValidSession);
}
@@ -302,7 +301,7 @@ public class YavscApiClientTests
[Fact]
public async Task TrySilentLoginAsync_returns_true_when_access_token_still_valid()
{
- using var authority = await OIDCStubAuthority.StartAsync();
+ using var authority = await OidcStubAuthority.StartAsync();
using var apiServer = new StubApiServer();
await apiServer.StartAsync();
@@ -315,7 +314,7 @@ public class YavscApiClientTests
{
await LoginWithBrowserAsync(client, browser.CreateBrowser());
// Login fresh → access token is far from expiry.
- var ok = await client.TrySilentLoginAsync(null, TestContext.Current.CancellationToken);
+ var ok = await client.TrySilentLoginAsync();
Assert.True(ok);
Assert.True(client.HasValidSession);
}
@@ -328,7 +327,7 @@ public class YavscApiClientTests
[Fact]
public async Task TrySilentLoginAsync_returns_true_when_refresh_succeeds()
{
- using var authority = await OIDCStubAuthority.StartAsync();
+ using var authority = await OidcStubAuthority.StartAsync();
using var apiServer = new StubApiServer();
await apiServer.StartAsync();
@@ -352,13 +351,13 @@ public class YavscApiClientTests
// matches the disk: access expired, refresh still good.
var client = new YavscApiClient(settings, store);
- var reported = new System.Collections.Generic.List();
- var progress = new SyncProgress(reported);
+ var reported = new System.Collections.Generic.List();
+ var progress = new SyncProgress(reported);
- var ok = await client.TrySilentLoginAsync(progress, TestContext.Current.CancellationToken);
+ var ok = await client.TrySilentLoginAsync(progress);
Assert.True(ok, "silent refresh should succeed via the stub authority.");
- Assert.Contains(OIDCLoginPhase.ExchangingCode, reported);
- Assert.Equal(OIDCLoginPhase.Success, Last(reported));
+ Assert.Contains(OidcLoginPhase.ExchangingCode, reported);
+ Assert.Equal(OidcLoginPhase.Success, Last(reported));
}
finally
{
@@ -431,7 +430,7 @@ public class YavscApiClientTests
/// overload stays for tests that don't care about phase events.
///
private static async Task LoginWithBrowserAsync(
- YavscApiClient client, IBrowser browser, IProgress? progress = null)
+ YavscApiClient client, IBrowser browser, IProgress? progress = null)
{
var original = Platform.CreateBrowser;
try
diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs
index 134ae42f..65ce8b26 100644
--- a/src/PostIt/PostIt/App.axaml.cs
+++ b/src/PostIt/PostIt/App.axaml.cs
@@ -63,7 +63,6 @@ public partial class App : Application
services.AddTransient();
services.AddTransient();
services.AddTransient();
- services.AddTransient();
// ViewModels
services.AddSingleton(settings);
@@ -73,7 +72,6 @@ public partial class App : Application
services.AddTransient();
services.AddTransient();
services.AddTransient();
- services.AddTransient();
// Persistent session banner: one instance for the lifetime of
// the app so the same VM survives page navigation.
diff --git a/src/PostIt/PostIt/Controls/SignaturePadControl.cs b/src/PostIt/PostIt/Controls/SignaturePadControl.cs
deleted file mode 100644
index 87949d30..00000000
--- a/src/PostIt/PostIt/Controls/SignaturePadControl.cs
+++ /dev/null
@@ -1,204 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Avalonia;
-using Avalonia.Controls.Primitives;
-using Avalonia.Input;
-using PostIt.Models;
-
-namespace PostIt.Controls;
-
-///
-/// Pointer-driven capture surface that records a signature as a list
-/// of strokes, each stroke being a length-prefixed sequence of (x, y)
-/// coordinates normalised to [0, CoordinateMax].
-///
-/// The control is render-agnostic: it does not draw anything. The
-/// host view templates a (typically a
-/// Border) as PART_CaptureArea for pointer capture,
-/// and binds a separate visual layer (e.g. a Canvas) to
-/// for redraw. Keeping the control headless of
-/// rendering makes it usable from a headless test where no
-/// composition happens.
-///
-/// Wire format (see ):
-/// int[] = [k0, x0, y0, ..., k1, x0, y0, ...]
-/// with x, y ∈ [0, 10_000].
-///
-/// Threading: pointer events are dispatched on the UI thread, which
-/// is the only thread that ever mutates . The
-/// buffer is safe to read from any thread as long as no read
-/// straddles a pointer event — for cross-thread transfer use
-/// , which copies.
-///
-public class SignaturePadControl : TemplatedControl
-{
- ///
- /// Styled property pointing at the
- /// that receives pointer events. Set it in the control's
- /// template (PART_CaptureArea).
- ///
- public static readonly StyledProperty CaptureAreaProperty =
- AvaloniaProperty.Register(nameof(CaptureArea));
-
- public InputElement? CaptureArea
- {
- get => GetValue(CaptureAreaProperty);
- set => SetValue(CaptureAreaProperty, value);
- }
-
- ///
- /// Captured strokes in wire form. Exposed as a read-only view
- /// over the internal buffer. The buffer only mutates on the UI
- /// thread, between pointer events.
- ///
- public IReadOnlyList Strokes => _strokes;
-
- ///
- /// Raised when the user finishes a stroke (pointer release).
- /// The argument is a snapshot of the buffer at release time.
- ///
- public event EventHandler? StrokeCompleted;
-
- ///
- /// Raised when the buffer changes: at the end of every stroke
- /// and on . Mid-stroke points do not raise
- /// this event (pointer-move is too dense); bind a separate
- /// visual layer if you need a live preview.
- ///
- public event EventHandler? RedrawRequested;
-
- private readonly List _strokes = new(capacity: 256);
- private int _pendingPoints; // number of (x, y) pairs awaiting a length prefix
- private bool _capturing;
-
- protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
- {
- base.OnApplyTemplate(e);
-
- if (CaptureArea is { } previous)
- {
- previous.PointerPressed -= OnCapturePressed;
- previous.PointerMoved -= OnCaptureMoved;
- previous.PointerReleased -= OnCaptureReleased;
- }
-
- if (CaptureArea is { } area)
- {
- area.PointerPressed += OnCapturePressed;
- area.PointerMoved += OnCaptureMoved;
- area.PointerReleased += OnCaptureReleased;
- }
- }
-
- private void OnCapturePressed(object? sender, PointerPressedEventArgs e)
- {
- if (!e.GetCurrentPoint(CaptureArea).Properties.IsLeftButtonPressed) return;
- e.Pointer.Capture(CaptureArea);
- _capturing = true;
- _pendingPoints = 0;
- AppendPoint(e.GetPosition(CaptureArea));
- }
-
- private void OnCaptureMoved(object? sender, PointerEventArgs e)
- {
- if (!_capturing) return;
- AppendPoint(e.GetPosition(CaptureArea));
- }
-
- private void OnCaptureReleased(object? sender, PointerReleasedEventArgs e)
- {
- if (!_capturing) return;
- AppendPoint(e.GetPosition(CaptureArea));
- _capturing = false;
-
- if (_pendingPoints == 0)
- {
- // Press + immediate release without movement yields no
- // point at all (the press fired AppendPoint, so this
- // branch is unreachable — kept for clarity if a future
- // change skips the press append).
- return;
- }
-
- // Seal the current stroke by inserting its length at the
- // head of its slice. The slice is the trailing
- // 2 * _pendingPoints entries.
- int sliceStart = _strokes.Count - 2 * _pendingPoints;
- _strokes.Insert(sliceStart, _pendingPoints);
- _pendingPoints = 0;
-
- StrokeCompleted?.Invoke(this, Snapshot());
- RedrawRequested?.Invoke(this, EventArgs.Empty);
- }
-
- private void AppendPoint(Point p)
- {
- var (nx, ny) = Normalise(p);
- _strokes.Add(nx);
- _strokes.Add(ny);
- _pendingPoints++;
- }
-
- private (int x, int y) Normalise(Point p)
- {
- if (CaptureArea is null) return (0, 0);
- var bounds = CaptureArea.Bounds;
- double w = bounds.Width;
- double h = bounds.Height;
- if (w <= 0 || h <= 0) return (0, 0);
- int nx = (int)Math.Round(Math.Clamp(p.X / w, 0.0, 1.0) * SignaturePadData.CoordinateMax);
- int ny = (int)Math.Round(Math.Clamp(p.Y / h, 0.0, 1.0) * SignaturePadData.CoordinateMax);
- return (nx, ny);
- }
-
- ///
- /// Forget every captured stroke. Raises .
- ///
- public void Clear()
- {
- _strokes.Clear();
- _pendingPoints = 0;
- _capturing = false;
- RedrawRequested?.Invoke(this, EventArgs.Empty);
- }
-
- ///
- /// Defensive copy of the current buffer wrapped in a
- /// . Cheap; call only when the
- /// view needs to ship the data off (e.g. to a backend).
- ///
- public SignaturePadData Snapshot() => new(_strokes.ToArray());
-
- // --- Test-only surface (visible to PostIt.Tests) -------------------
-
- ///
- /// Test hook: append a single normalised point without going
- /// through the pointer pipeline. Does not raise
- /// .
- ///
- internal void AppendPointForTest(int x, int y)
- {
- _strokes.Add(x);
- _strokes.Add(y);
- _pendingPoints++;
- }
-
- ///
- /// Test hook: seal the currently-pending stroke with a length
- /// prefix. Mirrors what does at
- /// pointer release time, including the
- /// and
- /// events, so test scenarios observe the same notification
- /// contract as production. Idempotent: a second call without
- /// intermediate appends is a no-op.
- ///
- internal void SealStrokeForTest()
- {
- if (_pendingPoints == 0) return;
- int sliceStart = _strokes.Count - 2 * _pendingPoints;
- _strokes.Insert(sliceStart, _pendingPoints);
- _pendingPoints = 0;
- StrokeCompleted?.Invoke(this, Snapshot());
- RedrawRequested?.Invoke(this, EventArgs.Empty);
- }
-}
diff --git a/src/PostIt/PostIt/Models/SignaturePadData.cs b/src/PostIt/PostIt/Models/SignaturePadData.cs
deleted file mode 100644
index 11568eac..00000000
--- a/src/PostIt/PostIt/Models/SignaturePadData.cs
+++ /dev/null
@@ -1,103 +0,0 @@
-namespace PostIt.Models;
-
-///
-/// Serialized form of a signature captured by
-/// .
-///
-/// Wire format (length-prefixed, normalised):
-///
-/// int[] = [k0, x00, y00, x01, y01, ..., x0_{k0-1}, y0_{k0-1},
-/// k1, x10, y10, x11, y11, ..., x1_{k1-1}, y1_{k1-1},
-/// ...]
-///
-///
-/// - k_i — number of (x, y) pairs in stroke i.
-/// - x, y — coordinates normalised to [0, CoordinateMax]
-/// (inclusive) on the control's client area.
-/// is 10_000 by default — a 4-decimal fixed-point fraction of
-/// the surface, which is enough to discriminate 0.01% of the diagonal
-/// on any reasonable screen and stays well inside int.
-/// - Total array length is even: each stroke contributes
-/// 1 + 2 * k_i integers, and 1 + 2k is always odd.
-/// Sum of 1 + 2k_i over strokes is therefore odd * N, which
-/// is odd when N is odd and even when N is even — so the overall
-/// "size pair" property is not enforced, only the per-stroke shape
-/// is. If the consumer needs a strictly even total, pad the last
-/// stroke with a duplicate terminal point (or use
-/// to drop the array entirely).
-///
-///
-/// Empty signature (no strokes) is represented by an empty array
-/// (length 0). A single dot — pen down + pen up at the same point —
-/// is a single stroke with k = 1: [1, x, y].
-///
-public sealed class SignaturePadData
-{
- ///
- /// Upper bound of normalised coordinates. 10_000 means a
- /// surface unit is represented as 0.0001 of the whole.
- ///
- public const int CoordinateMax = 10_000;
-
- ///
- /// Raw payload. See for the layout.
- /// Never null; an empty array means "no strokes".
- ///
- public int[] Strokes { get; }
-
- public SignaturePadData(int[] strokes)
- {
- if (strokes is null) throw new System.ArgumentNullException(nameof(strokes));
- Strokes = strokes;
- }
-
- /// True if no stroke has been captured.
- public bool IsEmpty => Strokes.Length == 0;
-
- ///
- /// Number of distinct strokes (pen-down / pen-up cycles).
- /// Returns 0 when is true.
- ///
- public int StrokeCount
- {
- get
- {
- if (Strokes.Length == 0) return 0;
- int n = 0;
- int i = 0;
- while (i < Strokes.Length)
- {
- int k = Strokes[i];
- // Defensive: a malformed entry is treated as 0 so we
- // never throw on read. The capture side never produces
- // these, this is only for robustness on the wire.
- if (k <= 0) return n;
- i += 1 + 2 * k;
- n++;
- }
- return n;
- }
- }
-
- ///
- /// Total number of (x, y) pairs across all strokes. Useful
- /// for sanity-checks and for displaying capture density
- /// without re-walking the wire format.
- ///
- public int PointCount
- {
- get
- {
- int n = 0;
- int i = 0;
- while (i < Strokes.Length)
- {
- int k = Strokes[i];
- if (k <= 0) break;
- n += k;
- i += 1 + 2 * k;
- }
- return n;
- }
- }
-}
diff --git a/src/PostIt/PostIt/Services/OidcLoginPhase.cs b/src/PostIt/PostIt/Services/OidcLoginPhase.cs
index c4787489..0add3d76 100644
--- a/src/PostIt/PostIt/Services/OidcLoginPhase.cs
+++ b/src/PostIt/PostIt/Services/OidcLoginPhase.cs
@@ -12,7 +12,7 @@ namespace PostIt.Services;
/// The set is deliberately small: each value is a milestone an
/// operator can grep for in logs / StatusMessage, not a heartbeat.
///
-public enum OIDCLoginPhase
+public enum OidcLoginPhase
{
/// No login in flight (or login has settled).
Idle,
diff --git a/src/PostIt/PostIt/Services/YavscApiClient.cs b/src/PostIt/PostIt/Services/YavscApiClient.cs
index 0a42773b..6710ebb8 100644
--- a/src/PostIt/PostIt/Services/YavscApiClient.cs
+++ b/src/PostIt/PostIt/Services/YavscApiClient.cs
@@ -91,15 +91,15 @@ public class YavscApiClient : IAsyncDisposable
/// for the human
/// text (URLs, error detail).
public async Task LoginInteractiveAsync(
- IProgress? progress = null,
+ IProgress? progress = null,
CancellationToken ct = default)
{
- progress?.Report(OIDCLoginPhase.Discovering);
+ progress?.Report(OidcLoginPhase.Discovering);
var browser = Platform.CreateBrowser?.Invoke();
if (browser is null)
{
- progress?.Report(OIDCLoginPhase.Error);
+ progress?.Report(OidcLoginPhase.Error);
throw new InvalidOperationException("No browser is available on this platform.");
}
@@ -114,20 +114,20 @@ public class YavscApiClient : IAsyncDisposable
// the moment we ask the browser to open (covers the entire
// user-driven window including the AwaitingCallback wait), and
// the moment we trade the code for tokens.
- progress?.Report(OIDCLoginPhase.OpeningBrowser);
+ progress?.Report(OidcLoginPhase.OpeningBrowser);
var result = await client.LoginAsync(new LoginRequest(), ct).ConfigureAwait(false);
if (result.IsError)
{
- progress?.Report(OIDCLoginPhase.Error);
+ progress?.Report(OidcLoginPhase.Error);
throw new InvalidOperationException($"OIDC login failed: {result.Error}");
}
- progress?.Report(OIDCLoginPhase.ExchangingCode);
+ progress?.Report(OidcLoginPhase.ExchangingCode);
if (string.IsNullOrEmpty(result.RefreshToken))
{
- progress?.Report(OIDCLoginPhase.Error);
+ progress?.Report(OidcLoginPhase.Error);
throw new InvalidOperationException(
"Missing refresh_token — vérifie le scope 'offline_access'.");
}
@@ -139,7 +139,7 @@ public class YavscApiClient : IAsyncDisposable
IdToken: result.IdentityToken);
_store.Save(_tokens);
- progress?.Report(OIDCLoginPhase.Success);
+ progress?.Report(OidcLoginPhase.Success);
}
///
@@ -152,7 +152,7 @@ public class YavscApiClient : IAsyncDisposable
/// phase and returns false so the UI can keep going.
///
public async Task TrySilentLoginAsync(
- IProgress? progress = null,
+ IProgress? progress = null,
CancellationToken ct = default)
{
if (!HasValidSession) return false;
@@ -161,7 +161,7 @@ public class YavscApiClient : IAsyncDisposable
// Access token still has plenty of life — nothing to do.
if (_tokens.AccessTokenExpiresAt - DateTimeOffset.UtcNow > RefreshSkew)
{
- progress?.Report(OIDCLoginPhase.Success);
+ progress?.Report(OidcLoginPhase.Success);
return true;
}
@@ -172,19 +172,19 @@ public class YavscApiClient : IAsyncDisposable
// the user back to the login page.
try
{
- progress?.Report(OIDCLoginPhase.ExchangingCode);
+ progress?.Report(OidcLoginPhase.ExchangingCode);
await ForceRefreshAsync(ct).ConfigureAwait(false);
- progress?.Report(OIDCLoginPhase.Success);
+ progress?.Report(OidcLoginPhase.Success);
return true;
}
catch (RefreshFailedException)
{
- progress?.Report(OIDCLoginPhase.Idle);
+ progress?.Report(OidcLoginPhase.Idle);
return false;
}
catch
{
- progress?.Report(OIDCLoginPhase.Idle);
+ progress?.Report(OidcLoginPhase.Idle);
return false;
}
}
@@ -205,16 +205,6 @@ public class YavscApiClient : IAsyncDisposable
return dto!;
}
- ///
- /// Call a JSON endpoint with no request body while still allowing a
- /// positional cancellation token argument.
- ///
- public Task CallAsync(
- HttpMethod method,
- string path,
- CancellationToken ct)
- => CallAsync(method, path, body: null, ct);
-
/// Call an endpoint that returns no useful body (DELETE, etc.).
public async Task CallAsync(
HttpMethod method,
@@ -226,16 +216,6 @@ public class YavscApiClient : IAsyncDisposable
response.EnsureSuccessStatusCode();
}
- ///
- /// Call an endpoint with no request body while still allowing a
- /// positional cancellation token argument.
- ///
- public Task CallAsync(
- HttpMethod method,
- string path,
- CancellationToken ct)
- => CallAsync(method, path, body: null, ct);
-
private async Task SendAsync(
HttpMethod method, string path, object? body, CancellationToken ct)
{
diff --git a/src/PostIt/PostIt/ViewLocator.cs b/src/PostIt/PostIt/ViewLocator.cs
index e6d0e91a..9a05aa84 100644
--- a/src/PostIt/PostIt/ViewLocator.cs
+++ b/src/PostIt/PostIt/ViewLocator.cs
@@ -29,7 +29,6 @@ public class ViewLocator : IDataTemplate
SettingsPageViewModel => _services.GetRequiredService(),
LoginPageViewModel => _services.GetRequiredService(),
HomePageViewModel => _services.GetRequiredService(),
- SignaturePageViewModel => _services.GetRequiredService(),
_ => new TextBlock { Text = $"No view for {data.GetType().Name}" }
};
}
diff --git a/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs b/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs
index a6ed595d..009d67f2 100644
--- a/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs
@@ -105,8 +105,8 @@ public partial class LoginPageViewModel : ViewModelBase
/// callback hand-off: when AwaitingCallback never resolves,
/// the OS never re-launched PostIt with the postit:// URL.
///
- private OIDCLoginPhase _phase = OIDCLoginPhase.Idle;
- public OIDCLoginPhase Phase
+ private OidcLoginPhase _phase = OidcLoginPhase.Idle;
+ public OidcLoginPhase Phase
{
get => _phase;
private set
@@ -122,13 +122,13 @@ public partial class LoginPageViewModel : ViewModelBase
///
public string PhaseLabel => _phase switch
{
- OIDCLoginPhase.Idle => "En attente",
- OIDCLoginPhase.Discovering => "Découverte OIDC…",
- OIDCLoginPhase.OpeningBrowser => "Ouverture du navigateur…",
- OIDCLoginPhase.AwaitingCallback => "En attente du callback postit://…",
- OIDCLoginPhase.ExchangingCode => "Échange du code contre les jetons…",
- OIDCLoginPhase.Success => "Connecté",
- OIDCLoginPhase.Error => "Erreur",
+ OidcLoginPhase.Idle => "En attente",
+ OidcLoginPhase.Discovering => "Découverte OIDC…",
+ OidcLoginPhase.OpeningBrowser => "Ouverture du navigateur…",
+ OidcLoginPhase.AwaitingCallback => "En attente du callback postit://…",
+ OidcLoginPhase.ExchangingCode => "Échange du code contre les jetons…",
+ OidcLoginPhase.Success => "Connecté",
+ OidcLoginPhase.Error => "Erreur",
_ => _phase.ToString(),
};
@@ -275,7 +275,7 @@ public partial class LoginPageViewModel : ViewModelBase
// The progress sink drives Phase / PhaseLabel; StatusMessage
// keeps the text detail (URLs, error messages). Same
// underlying flow, two views.
- var progress = new Progress(p => Phase = p);
+ var progress = new Progress(p => Phase = p);
await LoginInteractiveCoreAsync(_api, progress);
IsBusy = false;
@@ -299,7 +299,7 @@ public partial class LoginPageViewModel : ViewModelBase
///
private async Task LoginInteractiveCoreAsync(
YavscApiClient api,
- IProgress? progress = null)
+ IProgress? progress = null)
{
var original = Platform.CreateBrowser;
try
diff --git a/src/PostIt/PostIt/ViewModels/SignaturePageViewModel.cs b/src/PostIt/PostIt/ViewModels/SignaturePageViewModel.cs
deleted file mode 100644
index b4b37974..00000000
--- a/src/PostIt/PostIt/ViewModels/SignaturePageViewModel.cs
+++ /dev/null
@@ -1,185 +0,0 @@
-using System;
-using System.IO;
-using System.Text;
-using System.Text.Json;
-using System.Threading.Tasks;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-using PostIt.Controls;
-using PostIt.Models;
-
-namespace PostIt.ViewModels;
-
-///
-/// Backing state for .
-///
-/// The page exists to produce a
-/// (length-prefixed normalised int[]) from a human signature drawn
-/// with the mouse (Desktop) or finger (touch / Android). The page
-/// is a recipient of an external trigger — a SignalR push from
-/// Yavsc.Org telling PostIt "a devis has been sent, sign here" —
-/// so it intentionally has no first-class entry point in
-/// . The only "open" affordance today is a
-/// dev-only shortcut on the blog editor, marked for removal once
-/// the SignalR handler lands.
-///
-/// Output path is the platform-friendly per-user data directory
-/// (XDG_DATA_HOME / AppData / NSDocumentDirectory on iOS). Files
-/// are JSON, one per capture, named
-/// signature-{yyyyMMdd-HHmmssfff}.json. This is a stop-gap
-/// until the Yavsc.Org endpoint exists; the contract there will
-/// be POST /api/signature/{devisId} with this same payload.
-///
-public partial class SignaturePageViewModel : ViewModelBase
-{
- ///
- /// Default capture surface, in DIPs. 3:1 ratio matches a
- /// signature line at the bottom of an A4 contract.
- ///
- public const double DefaultWidth = 600;
- public const double DefaultHeight = 200;
-
- [ObservableProperty]
- public partial string StatusMessage { get; set; } = "Prêt.";
-
- [ObservableProperty]
- public partial int StrokeCount { get; set; }
-
- [ObservableProperty]
- public partial int PointCount { get; set; }
-
- [ObservableProperty]
- public partial string? LastCapturedPath { get; set; }
-
- public double Width { get; }
- public double Height { get; }
-
- private SignaturePadControl? _control;
-
- public override bool CanNavigateNext
- {
- get => false;
- protected set { _ = value; }
- }
-
- public override bool CanNavigatePrevious
- {
- get => true;
- protected set { _ = value; }
- }
-
- public SignaturePageViewModel()
- : this(DefaultWidth, DefaultHeight)
- {
- }
-
- public SignaturePageViewModel(double width, double height)
- {
- if (width <= 0) throw new ArgumentOutOfRangeException(nameof(width));
- if (height <= 0) throw new ArgumentOutOfRangeException(nameof(height));
- Width = width;
- Height = height;
- }
-
- ///
- /// Bind a freshly-constructed (or re-templated) control to this
- /// VM. Called from the view's code-behind once the control has
- /// been added to the visual tree and its template applied (so
- /// is wired).
- ///
- public void Attach(SignaturePadControl control)
- {
- if (control is null) throw new ArgumentNullException(nameof(control));
- Detach();
- _control = control;
- _control.RedrawRequested += OnRedraw;
- _control.StrokeCompleted += OnStrokeCompleted;
- RefreshCounts();
- }
-
- public void Detach()
- {
- if (_control is null) return;
- _control.RedrawRequested -= OnRedraw;
- _control.StrokeCompleted -= OnStrokeCompleted;
- _control = null;
- }
-
- private void OnStrokeCompleted(object? sender, SignaturePadData data)
- {
- StatusMessage = $"Trait terminé. {data.StrokeCount} trait(s).";
- RefreshCounts();
- }
-
- private void OnRedraw(object? sender, EventArgs e) => RefreshCounts();
-
- private void RefreshCounts()
- {
- if (_control is null) return;
- var snap = _control.Snapshot();
- StrokeCount = snap.StrokeCount;
- PointCount = snap.PointCount;
- }
-
- [RelayCommand]
- public void Clear()
- {
- _control?.Clear();
- StatusMessage = "Effacé.";
- RefreshCounts();
- }
-
- [RelayCommand]
- public async Task CaptureAsync()
- {
- if (_control is null)
- {
- StatusMessage = "Contrôle non attaché.";
- return;
- }
-
- var data = _control.Snapshot();
- if (data.IsEmpty)
- {
- StatusMessage = "Rien à capturer.";
- return;
- }
-
- try
- {
- var path = WriteCapture(data);
- LastCapturedPath = path;
- StatusMessage = $"Capture enregistrée: {path}";
- }
- catch (Exception ex)
- {
- StatusMessage = $"Erreur: {ex.Message}";
- }
- await Task.CompletedTask;
- }
-
- private static string WriteCapture(SignaturePadData data)
- {
- var dir = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
- "PostIt", "signatures");
- Directory.CreateDirectory(dir);
-
- var fileName = $"signature-{DateTime.UtcNow:yyyyMMdd-HHmmssfff}.json";
- var path = Path.Combine(dir, fileName);
-
- var payload = new
- {
- format = "yavsc.signature/v1",
- coordinateMax = SignaturePadData.CoordinateMax,
- capturedAtUtc = DateTime.UtcNow,
- strokes = data.Strokes,
- strokeCount = data.StrokeCount,
- };
- File.WriteAllText(
- path,
- JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true }),
- Encoding.UTF8);
- return path;
- }
-}
diff --git a/src/PostIt/PostIt/Views/MainPage.axaml b/src/PostIt/PostIt/Views/MainPage.axaml
index c09c2f7a..b9e0308d 100644
--- a/src/PostIt/PostIt/Views/MainPage.axaml
+++ b/src/PostIt/PostIt/Views/MainPage.axaml
@@ -35,17 +35,6 @@
-
-
diff --git a/src/PostIt/PostIt/Views/MainPage.axaml.cs b/src/PostIt/PostIt/Views/MainPage.axaml.cs
index 1535d2f4..769234e3 100644
--- a/src/PostIt/PostIt/Views/MainPage.axaml.cs
+++ b/src/PostIt/PostIt/Views/MainPage.axaml.cs
@@ -1,8 +1,5 @@
using Avalonia;
using Avalonia.Controls;
-using Avalonia.Interactivity;
-using Microsoft.Extensions.DependencyInjection;
-using PostIt.ViewModels;
namespace PostIt.Views;
@@ -13,30 +10,4 @@ public partial class MainPage : ContentPage
InitializeComponent();
}
- ///
- /// DEV ONLY: temporary shortcut to open the signature capture
- /// page from the blog editor. The production entry point is a
- /// SignalR push from Yavsc.Org ("devis received, sign here"),
- /// which is the only path that carries the devis identifier
- /// needed to bind the capture to a specific contract.
- ///
- /// Remove this method and the corresponding button in
- /// MainPage.axaml.cs once the SignalR handler lands.
- ///
- private void OpenSignatureDev(object? sender, RoutedEventArgs e)
- {
- // Resolve via the App's DI container so the page gets
- // the canonical services (Api client, settings, ...).
- var app = Application.Current as App;
- var services = app?.Services;
- if (services is null) return;
-
- var page = services.GetRequiredService();
- page.DataContext = services.GetRequiredService();
-
- if (this.VisualRoot is MainWindow window)
- {
- _ = window.NavRoot.PushAsync(page);
- }
- }
}
diff --git a/src/PostIt/PostIt/Views/SignaturePage.axaml b/src/PostIt/PostIt/Views/SignaturePage.axaml
deleted file mode 100644
index d868f2a4..00000000
--- a/src/PostIt/PostIt/Views/SignaturePage.axaml
+++ /dev/null
@@ -1,77 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/PostIt/PostIt/Views/SignaturePage.axaml.cs b/src/PostIt/PostIt/Views/SignaturePage.axaml.cs
deleted file mode 100644
index da6b8b7b..00000000
--- a/src/PostIt/PostIt/Views/SignaturePage.axaml.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Controls.Shapes;
-using Avalonia.Media;
-using PostIt.Controls;
-using PostIt.ViewModels;
-
-namespace PostIt.Views;
-
-public partial class SignaturePage : ContentPage
-{
- private static readonly IBrush StrokeBrush = new SolidColorBrush(Color.FromRgb(0x10, 0x10, 0x10));
- private const double StrokeThickness = 2.0;
- private const double CoordinateMax = 10_000.0;
-
- private SignaturePageViewModel? _vm;
- private SignaturePadControl? _control;
-
- public SignaturePage()
- {
- InitializeComponent();
-
- // Wire the capture area: the Pad itself is the control, the
- // surrounding Border (PadFrame) is the hit-test region. We
- // set CaptureArea once the control's template has been
- // applied — for an inline control with no template, that
- // happens on first measure, which is guaranteed before
- // the user can interact, so attaching here is safe.
- _control = Pad;
- _control.CaptureArea = PadFrame;
-
- DataContextChanged += (_, _) => RebindViewModel(DataContext as SignaturePageViewModel);
- }
-
- private void RebindViewModel(SignaturePageViewModel? vm)
- {
- if (_vm is not null)
- {
- _vm.Detach();
- _control!.RedrawRequested -= OnRedrawRequested;
- }
-
- _vm = vm;
-
- if (_vm is null || _control is null) return;
-
- _vm.Attach(_control);
- _control.RedrawRequested += OnRedrawRequested;
- Repaint();
- }
-
- private void OnRedrawRequested(object? sender, EventArgs e) => Repaint();
-
- private void Repaint()
- {
- if (_control is null || InkLayer is null) return;
-
- InkLayer.Children.Clear();
- var w = PadFrame.Bounds.Width;
- var h = PadFrame.Bounds.Height;
- if (w <= 0 || h <= 0) return;
-
- var strokes = _control.Strokes;
- int i = 0;
- while (i < strokes.Count)
- {
- int k = strokes[i];
- if (k <= 0) break;
- i++; // skip the length prefix
-
- var poly = new Polyline
- {
- Stroke = StrokeBrush,
- StrokeThickness = StrokeThickness,
- StrokeLineCap = PenLineCap.Round,
- StrokeJoin = PenLineJoin.Round,
- };
- var pts = new List(k);
- for (int p = 0; p < k; p++)
- {
- int nx = strokes[i++];
- int ny = strokes[i++];
- pts.Add(new Point(nx / CoordinateMax * w, ny / CoordinateMax * h));
- }
- poly.Points = pts;
- InkLayer.Children.Add(poly);
- }
- }
-}
diff --git a/src/Yavsc.Abstract/Chat/ChatHubConstants.cs b/src/Yavsc.Abstract/Chat/ChatHubConstants.cs
index b54c00c7..e68c719d 100644
--- a/src/Yavsc.Abstract/Chat/ChatHubConstants.cs
+++ b/src/Yavsc.Abstract/Chat/ChatHubConstants.cs
@@ -14,11 +14,11 @@ namespace Yavsc.Abstract.Chat
public const string JustCreatedBy = "just created by ";
public const string LabYouNotOp = "you're no op.";
- public const string LabNoSuchUser = "No such user";
- public const string LabNoSuchChan = "No such chan";
+ public const string LabNoSuchUser = "No such user";
+ public const string LabNoSuchChan = "No such chan";
public const string HopWontKickOp = "Half operator cannot kick any operator";
public const string LabAuthChatUser = "Authenticated chat user";
public const string NoKickOnCop = "No, you won´t, you´ĺl never do kick a cop, it is the bad.";
- public const string LabNoJoinNoSend = "LabnoJoinNoSend";
+ public const string LabnoJoinNoSend = "LabnoJoinNoSend";
}
-}
+}
\ No newline at end of file
diff --git a/src/Yavsc.Server/Hubs/HubInputValidator.cs b/src/Yavsc.Abstract/Chat/HubInputValidator.cs
similarity index 81%
rename from src/Yavsc.Server/Hubs/HubInputValidator.cs
rename to src/Yavsc.Abstract/Chat/HubInputValidator.cs
index a6acac5a..e4b34941 100644
--- a/src/Yavsc.Server/Hubs/HubInputValidator.cs
+++ b/src/Yavsc.Abstract/Chat/HubInputValidator.cs
@@ -19,47 +19,40 @@
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see .
-using Microsoft.Extensions.Localization;
+using System;
+using System.Linq;
+using Yavsc;
-namespace Yavsc.Server.Hubs
+namespace Yavsc
{
public class HubInputValidator {
- public IStringLocalizer _locator;
- public HubInputValidator(IStringLocalizer locator) {
- _locator = locator;
- }
-
public Action NotifyUser {get;set;}
public bool ValidateRoomName (string roomName)
{
bool valid = ValidateStringLength(roomName,1,25);
if (valid) valid = IsLetterOrDigit(roomName);
- if (!valid) NotifyUser(NotificationTypes.Error, "roomName", _locator.GetString("InvalidRoomName"));
+ if (!valid) NotifyUser(NotificationTypes.Error, "roomName", ChatHubLabels.InvalidRoomName);
return valid;
}
public bool ValidateUserName (string userName)
{
bool valid = true;
-
+
if (userName.Length<1 || userName[0] == '?' && userName.Length<2) valid = false;
if (valid) {
string suname = (userName[0] == '?') ? userName.Substring(1) : userName;
if (valid) valid = ValidateStringLength(suname, 1,12);
if (valid) valid = IsLetterOrDigit(userName);
}
- if (!valid)
- NotifyUser(
- NotificationTypes.Error,
- "userName",
- _locator.GetString("InvalidUserName)"));
+ if (!valid) NotifyUser(NotificationTypes.Error, "userName" , ChatHubLabels.InvalidUserName);
return valid;
}
public bool ValidateMessage (string message)
{
if (!ValidateStringLength(message, 1, 10240))
{
- NotifyUser(NotificationTypes.Error, "message", _locator.GetString("InvalidMessage"));
+ NotifyUser(NotificationTypes.Error, "message", ChatHubLabels.InvalidMessage);
return false;
}
return true;
@@ -68,7 +61,7 @@ namespace Yavsc.Server.Hubs
{
if (!ValidateStringLength(reason, 1,240))
{
- NotifyUser(NotificationTypes.Error, "reason", "InvalidReason");
+ NotifyUser(NotificationTypes.Error, "reason", ChatHubLabels.InvalidReason);
return false;
}
return true;
diff --git a/src/Yavsc.Abstract/Interfaces/IBillingService.cs b/src/Yavsc.Abstract/Interfaces/IBillingService.cs
index 3ba7fc59..5c99212a 100644
--- a/src/Yavsc.Abstract/Interfaces/IBillingService.cs
+++ b/src/Yavsc.Abstract/Interfaces/IBillingService.cs
@@ -17,8 +17,8 @@ namespace Yavsc.Services
///
/// Renvoye la facture associée à une clé de facturation,
/// à partir du couple suivant :
- ///
- /// * un code de facturation
+ ///
+ /// * un code de facturation
/// (identifiant associé à un type de demande du client)
/// * un entier long identifiant la demande du client
/// (à une demande, on associe au maximum une seule facture)
@@ -26,10 +26,10 @@ namespace Yavsc.Services
/// Identifiant du type de facturation
/// Identifiant de la demande du client
/// La facture
- Task GetBillAsync(string billingCode, long queryId);
-
+ Task GetBillAsync(string billingCode, long queryId);
+
///
- /// Perfomer settings for the specified performer in the activity
+ /// Perfomer settings for the specified performer in the activity
///
/// activityCode
/// performer uid
diff --git a/src/Yavsc.Abstract/Resources/Yavsc.ChatHubLabels.Designer.cs b/src/Yavsc.Abstract/Resources/Yavsc.ChatHubLabels.Designer.cs
new file mode 100644
index 00000000..bdfc3776
--- /dev/null
+++ b/src/Yavsc.Abstract/Resources/Yavsc.ChatHubLabels.Designer.cs
@@ -0,0 +1,82 @@
+// ------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Mono Runtime Version: 4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+// ------------------------------------------------------------------------------
+
+namespace Yavsc {
+ using System;
+ using System.Reflection;
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ public partial class ChatHubLabels {
+
+ private static System.Resources.ResourceManager resourceMan;
+
+ private static System.Globalization.CultureInfo resourceCulture;
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.Equals(null, resourceMan)) {
+ System.Resources.ResourceManager temp = new System.Resources.ResourceManager(("Yavsc.Abstract.Resources." + "Yavsc.ChatHub"), typeof(ChatHubLabels).GetTypeInfo().Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ public static string Authenticated_chat_user {
+ get {
+ return ResourceManager.GetString("Authenticated chat user", resourceCulture);
+ }
+ }
+
+ public static string LabnoJoinNoSend {
+ get {
+ return ResourceManager.GetString("LabnoJoinNoSend", resourceCulture);
+ }
+ }
+
+ public static string InvalidRoomName {
+ get {
+ return ResourceManager.GetString("InvalidRoomName", resourceCulture);
+ }
+ }
+
+ public static string InvalidUserName {
+ get {
+ return ResourceManager.GetString("InvalidUserName", resourceCulture);
+ }
+ }
+
+ public static string InvalidMessage {
+ get {
+ return ResourceManager.GetString("InvalidMessage", resourceCulture);
+ }
+ }
+
+ public static string InvalidReason {
+ get {
+ return ResourceManager.GetString("InvalidReason", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Yavsc.Abstract/Workflow/INominativeQuery.cs b/src/Yavsc.Abstract/Workflow/INominativeQuery.cs
index d498e28d..f0adebdc 100644
--- a/src/Yavsc.Abstract/Workflow/INominativeQuery.cs
+++ b/src/Yavsc.Abstract/Workflow/INominativeQuery.cs
@@ -4,6 +4,8 @@ namespace Yavsc.Abstract.Workflow
{
public interface IDecidableQuery: ITrackedEntity, IQuery
{
-
+ bool Decided { get; set; }
+ bool Accepted { get; set; }
+
}
}
diff --git a/src/Yavsc.Abstract/Workflow/QueryStatus.cs b/src/Yavsc.Abstract/Workflow/QueryStatus.cs
index 86104376..e440ebbc 100644
--- a/src/Yavsc.Abstract/Workflow/QueryStatus.cs
+++ b/src/Yavsc.Abstract/Workflow/QueryStatus.cs
@@ -9,9 +9,12 @@ namespace Yavsc
public enum QueryStatus: int
{
Inserted,
+ OwnerValidated,
+ Visited,
Rejected,
Accepted,
InProgress,
+
// final states
Failed,
Success
diff --git a/src/Yavsc.Api/Controllers/Business/BillingController.cs b/src/Yavsc.Api/Controllers/Business/BillingController.cs
index 65a6dbcf..25844955 100644
--- a/src/Yavsc.Api/Controllers/Business/BillingController.cs
+++ b/src/Yavsc.Api/Controllers/Business/BillingController.cs
@@ -5,8 +5,6 @@ using Newtonsoft.Json;
using System.Security.Claims;
using Yavsc.Helpers;
using Yavsc.ViewModels;
-using Yavsc.Models.Billing;
-using Yavsc.Server.Models.FileSystem;
namespace Yavsc.ApiControllers
{
@@ -183,203 +181,5 @@ namespace Yavsc.ApiControllers
if (!fi.Exists) return NotFound(new { Error = "Professional signature not found" });
return File(fi.OpenRead(), "application/x-pdf", filename); ;
}
-
- ///
- /// Capture a signature for an estimate, in the JSON
- /// wire format produced by PostIt (see
- /// PostIt.Models.SignaturePadData). The legacy
- /// POST prosign / POST clisign endpoints
- /// take a PNG ; this one takes a
- /// JSON body so the capture happens entirely in-app on
- /// the client side, without a rasterisation step.
- ///
- /// The route is intentionally a sibling of the
- /// legacy endpoints, not a replacement: the legacy
- /// PNG-based flow stays in place to keep the TeX
- /// invoice templates (Bill_tex.cshtml,
- /// Estimate_tex.cshtml) working until the
- /// migration commit regenerates PNGs from the JSON
- /// payload. The two flows share the
- /// table for storage but not
- /// the URL surface.
- ///
- [HttpPost("estimate/{id:long}/sign")]
- [ValidateAntiForgeryToken]
- [Consumes("application/json")]
- [ProducesResponseType(StatusCodes.Status201Created)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task Sign(
- [FromRoute] long id,
- [FromBody] SignatureSubmission body,
- CancellationToken token)
- {
- if (body is null) return BadRequest(new { Error = "missing body" });
- if (body.Strokes is null) return BadRequest(new { Error = "missing strokes" });
- if (string.IsNullOrEmpty(body.SignerUserId))
- return BadRequest(new { Error = "missing signerUserId" });
-
- var estimate = await dbContext.Estimates
- .Include(e => e.Client)
- .FirstOrDefaultAsync(e => e.Id == id, token);
- if (estimate is null) return NotFound(new { Error = "estimate not found" });
-
- // The signer is identified by userId in the body, not
- // by the bearer token, because the OAuth scope we
- // carry is for the API client (PostIt), not the end
- // user. We trust the body's userId to match either
- // Owner or Client, and reject everything else.
- var userId = body.SignerUserId;
- if (userId != estimate.OwnerId && userId != estimate.ClientId)
- return Forbid();
-
- // Map userId → type. The Pro/Client split is the
- // same one the legacy prosign/clisign endpoints use;
- // keeping the rule here means the Signature table
- // and the legacy ProviderValidationDate/ClientValidationDate
- // columns can co-exist without contradicting each other.
- var type = userId == estimate.OwnerId
- ? SignatureType.Pro
- : SignatureType.Client;
-
- var payload = new SignaturePadPayload
- {
- CoordinateMax = body.CoordinateMax,
- CapturedAtUtc = body.CapturedAtUtc ?? DateTime.UtcNow,
- Strokes = body.Strokes,
- };
-
- // Disk write first: a disk failure shouldn't leave
- // a Signature row pointing at a file that doesn't
- // exist. The file helper throws on filesystem
- // problems and propagates here.
- FileReceivedInfo fi;
- try
- {
- fi = await User.ReceiveEstimateSignatureAsync(id, type, payload, token);
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "estimate {Id}: signature file write failed", id);
- return BadRequest(new { Error = "file write failed", Detail = ex.Message });
- }
-
- // Find-or-add: the (EstimateId, Type) pair is
- // unique, so a second POST for the same side of the
- // estimate replaces the previous signature. EF
- // translates this into a single UPDATE when the
- // row exists and an INSERT otherwise; the unique
- // index in ApplicationDbContext is the
- // database-level guarantee that the contract
- // holds if two requests race.
- var signature = await dbContext.Signatures
- .FirstOrDefaultAsync(s => s.EstimateId == id && s.Type == type, token);
-
- if (signature is null)
- {
- signature = new Signature
- {
- EstimateId = id,
- SignerId = userId,
- Type = type,
- };
- dbContext.Signatures.Add(signature);
- }
- else
- {
- // Roll the signer's quota back by the size of
- // the file we're about to orphan: the old
- // FilePath is no longer referenced once we
- // overwrite FilePath below.
- try
- {
- var orphan = new FileInfo(signature.FilePath);
- if (orphan.Exists)
- {
- var signerForOrphan = await dbContext.Users
- .FirstOrDefaultAsync(u => u.Id == userId, token);
- if (signerForOrphan is not null)
- signerForOrphan.DiskUsage =
- Math.Max(0, signerForOrphan.DiskUsage - orphan.Length);
- }
- }
- catch { /* best effort — the file is being replaced anyway */ }
- }
-
- signature.SignerId = userId;
- signature.CoordinateMax = payload.CoordinateMax;
- signature.Strokes = payload.Strokes;
- signature.CapturedAtUtc = payload.CapturedAtUtc;
- signature.FilePath = Path.Combine(fi.DestDir, fi.FileName);
-
- // Bump the signer's quota. The Signature row's
- // SignerId is the IdentityUser.Id (a string), so we
- // look up by Id and not by username.
- var signer = await dbContext.Users
- .FirstOrDefaultAsync(u => u.Id == userId, token);
- if (signer is not null)
- {
- signer.DiskUsage += new FileInfo(signature.FilePath).Length;
- }
-
- try
- {
- await dbContext.SaveChangesAsync(token);
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "estimate {Id}: signature db write failed", id);
- // Best-effort rollback: remove the file we wrote
- // so disk and db don't disagree.
- try { System.IO.File.Delete(signature.FilePath); }
- catch { /* swallow — the row will be re-orphaned, the user re-signs */ }
- return BadRequest(new { Error = "db write failed", Detail = ex.Message });
- }
-
- var location = Url.Action(nameof(Sign), new { id })
- ?? $"/api/bill/estimate/{id}/sign";
- return Created(location, new
- {
- id = signature.Id,
- estimateId = signature.EstimateId,
- type = signature.Type.ToString(),
- capturedAtUtc = signature.CapturedAtUtc,
- coordinateMax = signature.CoordinateMax,
- });
- }
}
}
-
-///
-/// JSON body of POST /api/bill/estimate/{id}/sign. The
-/// shape mirrors what PostIt sends; the signerUserId
-/// field disambiguates which side of the estimate signed
-/// because the bearer token belongs to the PostIt OAuth
-/// client, not the end user.
-///
-public class SignatureSubmission
-{
- ///
- /// ApplicationUser.Id of the signer. Must equal
- /// Estimate.OwnerId for a Pro signature or
- /// Estimate.ClientId for a Client signature.
- ///
- public string SignerUserId { get; set; }
-
- ///
- /// Wire-format strokes. See
- /// PostIt.Models.SignaturePadData.
- ///
- public int[] Strokes { get; set; } = Array.Empty();
-
- public int CoordinateMax { get; set; } = 10_000;
-
- ///
- /// Client-reported capture time. The server may override
- /// this with DateTime.UtcNow if the client is
- /// caught lying about clock skew, but the default is to
- /// trust the client.
- ///
- public DateTime? CapturedAtUtc { get; set; }
-}
diff --git a/src/Yavsc.Api/Controllers/Business/BookQueryApiController.cs b/src/Yavsc.Api/Controllers/Business/BookQueryApiController.cs
index 24fa3c28..ac1630e8 100644
--- a/src/Yavsc.Api/Controllers/Business/BookQueryApiController.cs
+++ b/src/Yavsc.Api/Controllers/Business/BookQueryApiController.cs
@@ -41,7 +41,7 @@ namespace Yavsc.Controllers
{
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
var now = DateTime.Now;
-
+
var result = _context.RdvQueries.Include(c => c.Location).
Include(c => c.Client).Where(c => c.PerformerId == uid && c.Id < maxId && c.EventDate > now
&& c.ValidationDate == null).
@@ -49,12 +49,12 @@ namespace Yavsc.Controllers
{
Client = new ClientProviderInfo {
UserName = c.Client.UserName,
- UserId = c.ClientId,
+ UserId = c.ClientId,
Avatar = c.Client.Avatar },
Location = c.Location,
EventDate = c.EventDate,
Id = c.Id,
- Previsional = c.Provisional,
+ Previsional = c.Previsional,
Reason = c.Reason,
ActivityCode = c.ActivityCode,
BillingCode = BillingCodes.Rdv
diff --git a/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs b/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs
index b91cba51..935de524 100644
--- a/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs
+++ b/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs
@@ -34,8 +34,8 @@ namespace Yavsc.ApiControllers
if (queryId == 0) return BadRequest("queryId");
var billing = BillingService.GetBillable(dbContext, billingCode, queryId);
if (billing == null) return BadRequest();
-
- billing.Status = QueryStatus.Rejected;
+ billing.Decided = true;
+ billing.Accepted = false;
dbContext.SaveChanges();
return Ok();
}
@@ -47,7 +47,8 @@ namespace Yavsc.ApiControllers
if (queryId == 0) return BadRequest("queryId");
var billing = BillingService.GetBillable(dbContext, billingCode, queryId);
if (billing == null) return BadRequest();
- billing.Status = QueryStatus.Accepted;
+ billing.Accepted = true;
+ billing.Decided = true;
dbContext.SaveChanges();
return Ok();
}
diff --git a/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs b/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs
index 927c0acc..12a93758 100644
--- a/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs
+++ b/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs
@@ -41,7 +41,7 @@ namespace Yavsc.ApiControllers
// user, as a client
public IActionResult Index()
{
-
+
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
var now = DateTime.Now;
@@ -151,7 +151,7 @@ namespace Yavsc.ApiControllers
{
HairCutQuery query = await _context.HairCutQueries.Include(q => q.Client).
- Include(q => q.Client.PostalAddress).Include(q => q.Prestation).Include(q=>q.Regularization)
+ Include(q => q.Client.PostalAddress).Include(q => q.Prestation).Include(q=>q.Regularisation)
.SingleAsync(q => q.Id == id);
if (query.PaymentId!=null)
return new BadRequestObjectResult(new { error = "An existing payment process already exists" });
diff --git a/src/Yavsc.Blogs/Controllers/FileSystemStreamController.cs b/src/Yavsc.Blogs/Controllers/FileSystemStream.cs
similarity index 98%
rename from src/Yavsc.Blogs/Controllers/FileSystemStreamController.cs
rename to src/Yavsc.Blogs/Controllers/FileSystemStream.cs
index 23cf0cc6..799d6ffb 100644
--- a/src/Yavsc.Blogs/Controllers/FileSystemStreamController.cs
+++ b/src/Yavsc.Blogs/Controllers/FileSystemStream.cs
@@ -3,13 +3,13 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Yavsc.Attributes.Validation;
+using Yavsc.Helpers;
using Yavsc.Models;
using Yavsc.Models.Messaging;
using Yavsc.Services;
using Microsoft.AspNetCore.SignalR;
using Yavsc.Server.Helpers;
using static Yavsc.Blogs.Constants;
-using Yavsc.Server.Hubs;
namespace Yavsc.Blogs.Controllers
{
@@ -46,7 +46,7 @@ namespace Yavsc.Blogs.Controllers
}
logger.LogInformation("validated: api/stream/Put: "+filename);
var userName = User.GetUserName();
-
+
string url = string.Format(
"{0}/{1}/{2}",
Config.UserFilesOptions.RequestPath.ToUriComponent(),
@@ -54,7 +54,7 @@ namespace Yavsc.Blogs.Controllers
filename
);
-
+
string destDir = HttpContext.User.EnsureDestinationDirectory(filePath);
logger.LogInformation($"Saving flow to {destDir}");
var userId = User.GetUserId();
@@ -65,7 +65,7 @@ namespace Yavsc.Blogs.Controllers
sender = userName,
url = url,
}, $"{userName} is starting a stream!");
-
+
await liveProcessor.AcceptStream(HttpContext, user, destDir, shortFileName);
return Ok();
}
diff --git a/src/Yavsc.Org.Tests/Controllers/CommandFormsControllerTests.cs b/src/Yavsc.Org.Tests/Controllers/CommandFormsControllerTests.cs
deleted file mode 100644
index f32893c7..00000000
--- a/src/Yavsc.Org.Tests/Controllers/CommandFormsControllerTests.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System.Net;
-using System.Net.Http;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Mvc.Testing;
-using Xunit;
-
-namespace Yavsc.Org.Tests.Controllers;
-
-public class CommandFormsControllerTests : IClassFixture
-{
- private readonly TestWebApplicationFactory _factory;
-
- public CommandFormsControllerTests(TestWebApplicationFactory factory)
- {
- _factory = factory;
- }
-
- private HttpClient CreateAdminClient()
- {
- var http = _factory.CreateClient(new WebApplicationFactoryClientOptions
- {
- HandleCookies = true,
- });
- http.DefaultRequestHeaders.Add(TestAuthPolicyProvider.HeaderName, TestAuthPolicyProvider.AdminRole);
- return http;
- }
-
- [Fact]
- public async Task Create_GET_returns_200_for_admin()
- {
- var http = CreateAdminClient();
- var response = await http.GetAsync("/CommandForms/Create", TestContext.Current.CancellationToken);
-
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
- Assert.Contains("Create", body);
- }
-}
diff --git a/src/Yavsc.Org.Tests/EstimateSignatureFileHelperTests.cs b/src/Yavsc.Org.Tests/EstimateSignatureFileHelperTests.cs
deleted file mode 100644
index 9de881af..00000000
--- a/src/Yavsc.Org.Tests/EstimateSignatureFileHelperTests.cs
+++ /dev/null
@@ -1,134 +0,0 @@
-using System;
-using System.IO;
-using System.Security.Claims;
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-using Xunit;
-using Yavsc.Models;
-using Yavsc.Models.Billing;
-using Yavsc.Server.Helpers;
-using Yavsc.Server.Models.FileSystem;
-
-namespace Yavsc.Org.Tests;
-
-///
-/// Tests for the static
-/// helper. Scope is intentionally narrow: the file-naming format,
-/// the strokes counter, and the on-disk write path. The controller
-/// (authz, db persistence, signalR notification) is out of scope
-/// for this commit and will get a dedicated integration test once
-/// the Yavsc.Api test project is set up.
-///
-public class EstimateSignatureFileHelperTests : IDisposable
-{
- private readonly string _tempRoot;
-
- public EstimateSignatureFileHelperTests()
- {
- // UserFilesDirName is a process-wide static; we redirect
- // it to a per-test temp dir so concurrent tests don't
- // collide and the host filesystem is not littered.
- _tempRoot = Path.Combine(
- Path.GetTempPath(),
- "yavsc-sig-tests-" + Guid.NewGuid().ToString("N"));
- Directory.CreateDirectory(_tempRoot);
- AbstractFileSystemHelpers.UserFilesDirName = _tempRoot;
- }
-
- public void Dispose()
- {
- try { Directory.Delete(_tempRoot, recursive: true); }
- catch { /* best effort — the OS will clean Temp eventually */ }
- }
-
- [Fact]
- public void FileNameFormat_lowercases_type_and_includes_estimateId_and_ticks()
- {
- var name = EstimateSignatureFileHelper.FileNameFormat(
- SignatureType.Pro, 42, 638_000_000_000_000_000L);
- Assert.Equal("sign-pro-42-638000000000000000.json", name);
-
- var cli = EstimateSignatureFileHelper.FileNameFormat(
- SignatureType.Client, 7, 1L);
- Assert.Equal("sign-client-7-1.json", cli);
- }
-
- [Theory]
- [InlineData(new int[] { }, 0)]
- [InlineData(new[] { 1, 100, 200 }, 1)]
- [InlineData(new[] { 2, 1, 2, 3, 4 }, 1)]
- [InlineData(new[] { 1, 1, 1, 2, 2, 3, 3 }, 2)]
- [InlineData(new[] { 0, 1, 2, 3 }, 0)] // malformed k=0: short-circuit
- public void ReceiveEstimateSignatureAsync_writes_a_v1_envelope(int[] strokes, int expectedStrokeCount)
- {
- // We don't read the count back from the helper (it's a
- // private method), but the JSON envelope must reflect
- // it; this verifies the public behaviour end-to-end.
- _ = expectedStrokeCount;
- // Arrange
- var user = MakeUser("alice");
- var payload = new SignaturePadPayload
- {
- CoordinateMax = 10_000,
- CapturedAtUtc = new DateTime(2026, 7, 4, 12, 0, 0, DateTimeKind.Utc),
- Strokes = strokes,
- };
-
- // Act
- var fi = Run(user, 123L, SignatureType.Pro, payload);
-
- // Assert: file exists, sits under the user's root, and
- // parses as a yavsc.signature/v1 envelope.
- var fullPath = Path.Combine(fi.DestDir, fi.FileName);
- Assert.True(File.Exists(fullPath), $"missing: {fullPath}");
-
- using var doc = JsonDocument.Parse(File.ReadAllText(fullPath));
- var root = doc.RootElement;
- Assert.Equal("yavsc.signature/v1", root.GetProperty("format").GetString());
- Assert.Equal(10_000, root.GetProperty("coordinateMax").GetInt32());
- Assert.Equal(123L, root.GetProperty("estimateId").GetInt64());
- Assert.Equal("Pro", root.GetProperty("type").GetString());
- Assert.Equal("alice", root.GetProperty("signerName").GetString());
- Assert.Equal(expectedStrokeCount, root.GetProperty("strokeCount").GetInt32());
- }
-
- [Fact]
- public async Task ReceiveEstimateSignatureAsync_rejects_null_payload()
- {
- var user = MakeUser("bob");
- await Assert.ThrowsAsync(() =>
- EstimateSignatureFileHelper.ReceiveEstimateSignatureAsync(
- user, 1L, SignatureType.Pro, payload: null!));
- }
-
- [Fact]
- public async Task ReceiveEstimateSignatureAsync_rejects_non_positive_estimateId()
- {
- var user = MakeUser("bob");
- var payload = new SignaturePadPayload { Strokes = new[] { 1, 100, 100 } };
- await Assert.ThrowsAsync(() =>
- EstimateSignatureFileHelper.ReceiveEstimateSignatureAsync(
- user, 0L, SignatureType.Pro, payload));
- }
-
- // --- helpers ----------------------------------------------------
-
- private static FileReceivedInfo Run(
- ClaimsPrincipal user, long estimateId, SignatureType type, SignaturePadPayload payload)
- {
- // The helper is async; tests that don't care about the
- // result can call it sync via .GetAwaiter().GetResult()
- // because we know it never throws in the happy path.
- return EstimateSignatureFileHelper
- .ReceiveEstimateSignatureAsync(user, estimateId, type, payload, CancellationToken.None)
- .GetAwaiter().GetResult();
- }
-
- private static ClaimsPrincipal MakeUser(string username)
- {
- return new ClaimsPrincipal(new ClaimsIdentity(
- new[] { new Claim(ClaimTypes.Name, username) },
- authenticationType: "test"));
- }
-}
diff --git a/src/Yavsc.Org.Tests/NonRegression/BillingServiceTests.cs b/src/Yavsc.Org.Tests/NonRegression/BillingServiceTests.cs
index bf40438a..0309ba8e 100644
--- a/src/Yavsc.Org.Tests/NonRegression/BillingServiceTests.cs
+++ b/src/Yavsc.Org.Tests/NonRegression/BillingServiceTests.cs
@@ -36,11 +36,11 @@ namespace Yavsc
{
WorkflowHelpers.ConfigureBillingService();
- var firstRegistrar = new Func((db, id) =>
- db.HairCutQueries.Include(q => q.Prestation).Include(q => q.Regularization).Single(q => q.Id == id));
+ var firstRegistrar = new Func((db, id) =>
+ db.HairCutQueries.Include(q => q.Prestation).Include(q => q.Regularisation).Single(q => q.Id == id));
const string testCode = "Brush";
-
+
Assert.Throws(() =>
WorkflowHelpers.RegisterBilling(testCode, firstRegistrar));
}
diff --git a/src/Yavsc.Org/Controllers/Accounting/ManageController.cs b/src/Yavsc.Org/Controllers/Accounting/ManageController.cs
index 4f32a438..9e3b4310 100644
--- a/src/Yavsc.Org/Controllers/Accounting/ManageController.cs
+++ b/src/Yavsc.Org/Controllers/Accounting/ManageController.cs
@@ -93,10 +93,10 @@ namespace Yavsc.Controllers
: "";
var user = await GetCurrentUserAsync();
-
+
long pc = _dbContext.BlogSpot.Count(x => x.AuthorId == user.Id);
-
-
+
+
var model = new IndexViewModel
{
@@ -123,14 +123,14 @@ namespace Yavsc.Controllers
AllowMonthlyEmail = user.AllowMonthlyEmail,
Address = user.PostalAddress?.Address
};
-
+
model.HaveProfessionalSettings = _dbContext.Performers.Any(x => x.PerformerId == user.Id);
var usrActs = _dbContext.UserActivities.Include(a=>a.Does).Where(a=> a.UserId == user.Id).ToArray();
// TODO remember me who this magical a.Settings is built
var usrActToSet = usrActs.Where( a => ( a.Settings == null && a.Does.SettingsClassName != null )).ToArray();
model.HaveActivityToConfigure = usrActToSet .Count()>0;
model.Activity = _dbContext.UserActivities.Include(a=>a.Does).Where(u=>u.UserId == user.Id).ToList();
-
+
return View(model);
}
@@ -152,7 +152,7 @@ namespace Yavsc.Controllers
var user = await GetCurrentUserAsync();
user.AllowMonthlyEmail = model.Allow;
await this._dbContext.SaveChangesAsync(User.GetUserId());
-
+
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.SetMonthlyEmailSuccess });
}
@@ -302,8 +302,8 @@ namespace Yavsc.Controllers
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
var calendars = await _calendarManager.GetCalendarsAsync(pageToken);
- return View(new SetGoogleCalendarViewModel {
- ReturnUrl = returnUrl,
+ return View(new SetGoogleCalendarViewModel {
+ ReturnUrl = returnUrl,
Calendars = calendars
});
}
@@ -343,9 +343,9 @@ namespace Yavsc.Controllers
)) return BadRequest(new { message = "data already present" });
user.BankInfo.Add(model);
-
+
_dbContext.Update(user);
-
+
await _dbContext.SaveChangesAsync();
}
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.SetBankInfoSuccess });
@@ -495,7 +495,7 @@ namespace Yavsc.Controllers
return View("Error");
}
var userLogins = await _userManager.GetLoginsAsync(user);
-
+
ViewBag.ShowRemoveButton = user.PasswordHash != null || userLogins.Count > 1;
return View(new ManageLoginsViewModel
diff --git a/src/Yavsc.Org/Controllers/Contracting/CommandFormsController.cs b/src/Yavsc.Org/Controllers/Contracting/CommandFormsController.cs
index f59666fc..4428105e 100644
--- a/src/Yavsc.Org/Controllers/Contracting/CommandFormsController.cs
+++ b/src/Yavsc.Org/Controllers/Contracting/CommandFormsController.cs
@@ -1,24 +1,20 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Localization;
+using Yavsc.Helpers;
using Yavsc.Models;
using Yavsc.Models.Workflow;
using Yavsc.Server.Helpers;
-using Yavsc.Services;
namespace Yavsc.Controllers
{
public class CommandFormsController : Controller
{
private readonly ApplicationDbContext _context;
- private readonly IStringLocalizer _localizer;
- public CommandFormsController(ApplicationDbContext context,
- IStringLocalizer localizer)
+ public CommandFormsController(ApplicationDbContext context)
{
_context = context;
- _localizer = localizer;
}
// GET: CommandForms
@@ -51,14 +47,11 @@ namespace Yavsc.Controllers
SetViewBag();
return View();
}
-
private void SetViewBag(CommandForm commandForm = null)
{
ViewBag.ActivityCode = new SelectList(_context.Activities, "Code", "Name", commandForm?.ActivityCode);
- ViewBag.ActionName = BillingService.Billing.Keys
- .Select((string b) => new SelectListItem { Value = b, Text = _localizer[b] }).ToList();
+ ViewBag.ActionName = _context.CommandForm.Select(c => new SelectListItem { Value = c.Id.ToString(), Text = c.Title, Selected = commandForm.Id == c.Id });
}
-
// POST: CommandForms/Create
[HttpPost]
[ValidateAntiForgeryToken]
diff --git a/src/Yavsc.Org/Controllers/Contracting/FrontOfficeController.cs b/src/Yavsc.Org/Controllers/Contracting/FrontOfficeController.cs
index b86d0ab4..b2a1cd22 100644
--- a/src/Yavsc.Org/Controllers/Contracting/FrontOfficeController.cs
+++ b/src/Yavsc.Org/Controllers/Contracting/FrontOfficeController.cs
@@ -36,16 +36,16 @@ namespace Yavsc.Controllers
public ActionResult Index()
{
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
- var now = DateTime.UtcNow;
+ var now = DateTime.Now;
var model = new FrontOfficeIndexViewModel
{
- EstimateToProduceCount = _context.RdvQueries.Where(c => c.PerformerId == uid && c.EventDate > now && c.Status == QueryStatus.Inserted
- && c.ValidationDate == null && !_context.Estimates.Any(e => e.CommandId == c.Id)).Count(),
- EstimateToHonorAsProCount = _context.RdvQueries.Where(c => c.PerformerId == uid && c.EventDate > now && c.Status == QueryStatus.Accepted
- && c.ValidationDate == null && _context.Estimates.Any(e => e.CommandId == c.Id )).Count(),
- EstimateToSignAsCliCount = _context.Estimates.Where(e => e.ClientId == uid && e.Query.Status == QueryStatus.Accepted).Count(),
-
+ EstimateToProduceCount = _context.RdvQueries.Where(c => c.PerformerId == uid && c.EventDate > now
+ && c.ValidationDate == null && !_context.Estimates.Any(e => (e.CommandId == c.Id && e.ProviderValidationDate != null))).Count(),
+ EstimateToSignAsProCount = _context.RdvQueries.Where(c => (c.PerformerId == uid && c.EventDate > now
+ && c.ValidationDate == null && _context.Estimates.Any(e => (e.CommandId == c.Id && e.ProviderValidationDate != null)))).Count(),
+ EstimateToSignAsCliCount = _context.Estimates.Where(e => e.ClientId == uid && e.ClientValidationDate == null).Count(),
+ BillToSignAsProCount = 0,
BillToSignAsCliCount = 0,
NewPayementsCount = 0
};
@@ -65,14 +65,14 @@ namespace Yavsc.Controllers
}
[AllowAnonymous]
- public async Task ListPerformersAsync(string activityCode)
+ public async Task HairCut(string id)
{
- if (activityCode == null)
+ if (id == null)
{
throw new NotImplementedException("No Activity code");
}
- ViewBag.Activity = await _context.Activities.FirstOrDefaultAsync(a => a.Code == activityCode);
- var result = await _context.ListPerformersAsync(_billing, activityCode);
+ ViewBag.Activity = await _context.Activities.FirstOrDefaultAsync(a => a.Code == id);
+ var result = await _context.ListPerformersAsync(_billing, id);
return View(result);
}
diff --git a/src/Yavsc.Org/Controllers/Contracting/GeneralSettingsController.cs b/src/Yavsc.Org/Controllers/Contracting/GeneralSettingsController.cs
index 5357a59e..1678f2a7 100644
--- a/src/Yavsc.Org/Controllers/Contracting/GeneralSettingsController.cs
+++ b/src/Yavsc.Org/Controllers/Contracting/GeneralSettingsController.cs
@@ -17,7 +17,7 @@ namespace Yavsc.Controllers
// GET: GeneralSettings
public async Task Index()
{
- return View(await _context.MusicLoverSettings.ToListAsync());
+ return View(await _context.GeneralSettings.ToListAsync());
}
// GET: GeneralSettings/Details/5
@@ -28,7 +28,7 @@ namespace Yavsc.Controllers
return NotFound();
}
- MusicLoverSettings generalSettings = await _context.MusicLoverSettings.SingleAsync(m => m.UserId == id);
+ MusicLoverSettings generalSettings = await _context.GeneralSettings.SingleAsync(m => m.UserId == id);
if (generalSettings == null)
{
return NotFound();
@@ -50,7 +50,7 @@ namespace Yavsc.Controllers
{
if (ModelState.IsValid)
{
- _context.MusicLoverSettings.Add(generalSettings);
+ _context.GeneralSettings.Add(generalSettings);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
@@ -65,7 +65,7 @@ namespace Yavsc.Controllers
return NotFound();
}
- MusicLoverSettings generalSettings = await _context.MusicLoverSettings.SingleAsync(m => m.UserId == id);
+ MusicLoverSettings generalSettings = await _context.GeneralSettings.SingleAsync(m => m.UserId == id);
if (generalSettings == null)
{
return NotFound();
@@ -96,7 +96,7 @@ namespace Yavsc.Controllers
return NotFound();
}
- MusicLoverSettings generalSettings = await _context.MusicLoverSettings.SingleAsync(m => m.UserId == id);
+ MusicLoverSettings generalSettings = await _context.GeneralSettings.SingleAsync(m => m.UserId == id);
if (generalSettings == null)
{
return NotFound();
@@ -110,8 +110,8 @@ namespace Yavsc.Controllers
[ValidateAntiForgeryToken]
public async Task DeleteConfirmed(string id)
{
- MusicLoverSettings generalSettings = await _context.MusicLoverSettings.SingleAsync(m => m.UserId == id);
- _context.MusicLoverSettings.Remove(generalSettings);
+ MusicLoverSettings generalSettings = await _context.GeneralSettings.SingleAsync(m => m.UserId == id);
+ _context.GeneralSettings.Remove(generalSettings);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
diff --git a/src/Yavsc.Org/Controllers/Generic/SettingsController.cs b/src/Yavsc.Org/Controllers/Generic/SettingsController.cs
index 24ac53a6..8c490a1c 100644
--- a/src/Yavsc.Org/Controllers/Generic/SettingsController.cs
+++ b/src/Yavsc.Org/Controllers/Generic/SettingsController.cs
@@ -6,6 +6,7 @@ namespace Yavsc.Controllers.Generic
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Models;
+ using Yavsc.Helpers;
using Yavsc.Server.Helpers;
using Yavsc.Services;
@@ -36,7 +37,7 @@ namespace Yavsc.Controllers.Generic
{
_context = context;
}
-
+
public async Task Index()
{
return View(await GetSettingsAsync(User.GetUserId()));
diff --git a/src/Yavsc.Org/Controllers/Haircut/HairCutCommandController.cs b/src/Yavsc.Org/Controllers/Haircut/HairCutCommandController.cs
index 80e5f80c..0a2610a8 100644
--- a/src/Yavsc.Org/Controllers/Haircut/HairCutCommandController.cs
+++ b/src/Yavsc.Org/Controllers/Haircut/HairCutCommandController.cs
@@ -49,7 +49,7 @@ namespace Yavsc.Controllers
this.haircutLocalizer = haircutLocalizer;
}
-
+
private async Task GetQuery(long id)
{
var query = await _context.HairCutQueries
@@ -58,7 +58,7 @@ namespace Yavsc.Controllers
.Include(x => x.Prestation)
.Include(x => x.PerformerProfile.Performer)
.Include(x => x.PerformerProfile.Performer.DeviceDeclaration)
- .Include(x => x.Regularization)
+ .Include(x => x.Regularisation)
.SingleAsync(m => m.Id == id);
query.SelectedProfile = await _context.BrusherProfile.SingleAsync(b => b.UserId == query.PerformerId);
return query;
@@ -82,11 +82,11 @@ namespace Yavsc.Controllers
}
var paymentInfo = await _context.ConfirmPayment(User.GetUserId(), PayerID, token);
ViewBag.paymentinfo = paymentInfo;
- command.Regularization = paymentInfo.DbContent;
+ command.Regularisation = paymentInfo.DbContent;
command.PaymentId = token;
bool paymentOk = false;
if (paymentInfo.DetailsFromPayPal != null)
- if (paymentInfo.DetailsFromPayPal.Ack == AckCodeType.SUCCESS)
+ if (paymentInfo.DetailsFromPayPal.Ack == AckCodeType.SUCCESS)
{
// FIXME Assert (command.ValidationDate == null)
if (command.ValidationDate == null) {
@@ -174,7 +174,7 @@ namespace Yavsc.Controllers
.Include(x => x.PerformerProfile)
.Include(x => x.Prestation)
.Include(x => x.PerformerProfile.Performer)
- .Include(x => x.Regularization)
+ .Include(x => x.Regularisation)
.SingleOrDefaultAsync(m => m.Id == id);
if (command == null)
{
@@ -224,7 +224,7 @@ namespace Yavsc.Controllers
.FirstOrDefault(
x => x.PerformerId == model.PerformerId
);
-
+
if (taintIds != null)
{
diff --git a/src/Yavsc.Org/Controllers/IT/ProjectController.cs b/src/Yavsc.Org/Controllers/IT/ProjectController.cs
index 965530f3..83ccc67a 100644
--- a/src/Yavsc.Org/Controllers/IT/ProjectController.cs
+++ b/src/Yavsc.Org/Controllers/IT/ProjectController.cs
@@ -18,7 +18,7 @@ namespace Yavsc.Controllers
private readonly ApplicationDbContext _context;
readonly IStringLocalizer _localizer;
readonly IStringLocalizer _bugLocalizer;
-
+
public ProjectController(ApplicationDbContext context,
IStringLocalizer localizer,
IStringLocalizer bugLocalizer
@@ -32,7 +32,7 @@ namespace Yavsc.Controllers
// GET: Project
public async Task Index()
{
- var applicationDbContext = _context.Project.Include(p => p.Client).Include(p => p.Context).Include(p => p.PerformerProfile).Include(p => p.Regularization).Include(p => p.Repository);
+ var applicationDbContext = _context.Project.Include(p => p.Client).Include(p => p.Context).Include(p => p.PerformerProfile).Include(p => p.Regularisation).Include(p => p.Repository);
return View(await applicationDbContext.ToListAsync());
}
diff --git a/src/Yavsc.Org/Directory.Packages.props b/src/Yavsc.Org/Directory.Packages.props
index fd16374b..6cd62331 100644
--- a/src/Yavsc.Org/Directory.Packages.props
+++ b/src/Yavsc.Org/Directory.Packages.props
@@ -4,14 +4,14 @@
-
-
+
+
-
-
-
-
+
+
+
+
diff --git a/src/Yavsc.Org/Extensions/EnumExtensions.cs b/src/Yavsc.Org/Extensions/EnumExtensions.cs
index 29fb0eee..ae733f20 100644
--- a/src/Yavsc.Org/Extensions/EnumExtensions.cs
+++ b/src/Yavsc.Org/Extensions/EnumExtensions.cs
@@ -1,5 +1,8 @@
+using System;
+using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
+using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Localization;
@@ -20,7 +23,7 @@ namespace Yavsc.Extensions
var typeInfo = type.GetTypeInfo();
var values = Enum.GetValues(type).Cast();
var items = new List();
-
+
foreach (var value in values)
{
items.Add(new SelectListItem {
diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs
index b29f37ce..b62a6567 100644
--- a/src/Yavsc.Org/Extensions/HostingExtensions.cs
+++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs
@@ -19,9 +19,6 @@ using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Diagnostics;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
@@ -44,7 +41,6 @@ using Yavsc.Settings;
using Yavsc.ViewModels.Auth;
using IdentityServer8.Models;
using IdentityServer8.EntityFramework.Mappers;
-using Yavsc.Server.Hubs;
namespace Yavsc.Extensions;
@@ -53,30 +49,6 @@ public static class HostingExtensions
{
private const string InMemoryProviderName = "InMemory";
- private static void IgnoreKnownFalsePositiveMigrationWarnings(DbContextOptionsBuilder options)
- {
- options.ConfigureWarnings(w =>
- w.Ignore(RelationalEventId.PendingModelChangesWarning));
- }
-
- private static async Task ApplyMigrationsAsync(IServiceProvider services, ILogger logger)
- where TContext : DbContext
- {
- var contextName = typeof(TContext).Name;
- var db = services.GetRequiredService();
-
- logger.LogInformation(
- "Applying database migrations for {DbContext} using provider {Provider}...",
- contextName,
- db.Database.ProviderName ?? "(null)");
-
- await db.Database.MigrateAsync();
-
- logger.LogInformation(
- "Database migrations applied successfully for {DbContext}.",
- contextName);
- }
-
public static WebApplication ConfigureWebAppServices(this WebApplicationBuilder builder)
{
builder.Services.AddSwaggerGen();
@@ -182,13 +154,6 @@ public static class HostingExtensions
{
options.UseNpgsql(connectionString,
options => options.MigrationsAssembly(typeof(Program).Assembly));
-
- // EF Core 10 can raise PendingModelChangesWarning at runtime
- // even when the snapshot and generated migrations are already
- // aligned on this codebase. Treat that known false positive as
- // non-fatal in every environment so production startup matches
- // the behavior already observed in development.
- IgnoreKnownFalsePositiveMigrationWarnings(options);
}
});
@@ -351,7 +316,6 @@ public static class HostingExtensions
{
b.UseNpgsql(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
- IgnoreKnownFalsePositiveMigrationWarnings(b);
}
// NOTE: don't b.UseSeeding(...) here — EF Core's UseSeeding
@@ -375,7 +339,6 @@ public static class HostingExtensions
{
b.UseNpgsql(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
- IgnoreKnownFalsePositiveMigrationWarnings(b);
}
};
@@ -926,13 +889,11 @@ public static class HostingExtensions
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
- await app.MigrateDatabaseAsync();
}
else
{
app.UseExceptionHandler("/Home/Error");
- logger.LogInformation("Running in production mode. Ensure the database is migrated.");
- await app.MigrateDatabaseAsync();
+ app.MigrateDatabase();
}
app.Use(async (context, next) =>
@@ -979,37 +940,26 @@ public static class HostingExtensions
return app;
}
- private static async Task MigrateDatabaseAsync(this IApplicationBuilder app)
+ private static void MigrateDatabase(this IApplicationBuilder app)
{
using (var serviceScope = app.ApplicationServices.GetRequiredService().CreateScope())
{
- var logger = serviceScope.ServiceProvider
- .GetRequiredService()
- .CreateLogger("Yavsc.Org.Migrations");
try
{
- using (var scope = app.ApplicationServices.CreateScope())
+ foreach (Type contextType in new Type[]
{
- await ApplyMigrationsAsync(scope.ServiceProvider, logger);
+ typeof(ApplicationDbContext)
+ })
+ {
+ ((DbContext)serviceScope.ServiceProvider
+ .GetRequiredService(contextType))
+ .Database.Migrate();
}
- EnsureCriticalSchema(serviceScope.ServiceProvider, logger);
}
- catch (Exception ex)
+ catch (InvalidOperationException ex)
{
app.Properties["DegradedDBContext"] = ex.Message;
- logger.LogError(
- ex,
- "Database migration failed for {DbContext}. App started in degraded mode.",
- nameof(ApplicationDbContext));
-
- // EF Core 10 may raise PendingModelChangesWarning as an exception.
- // Dump a concise model diff to make the mismatch actionable.
- if (ex is InvalidOperationException ioe
- && ioe.Message.Contains("PendingModelChangesWarning", StringComparison.Ordinal))
- {
- LogPendingModelChanges(serviceScope.ServiceProvider, logger);
- }
}
}
@@ -1022,88 +972,6 @@ public static class HostingExtensions
SeedConfigurationDatabase(app);
}
- private static void LogPendingModelChanges(IServiceProvider services, ILogger logger)
- {
- try
- {
- using var scope = services.CreateScope();
- var db = scope.ServiceProvider.GetRequiredService();
- var migrationsAssembly = db.GetService();
- var snapshotModel = migrationsAssembly.ModelSnapshot?.Model;
- if (snapshotModel is null)
- {
- logger.LogWarning("Pending-model diagnostic: no ModelSnapshot found for ApplicationDbContext.");
- return;
- }
-
- logger.LogWarning(
- "Pending-model diagnostic: provider={Provider}",
- db.Database.ProviderName ?? "(null)");
-
- var runtimeDeclDate = db.Model
- .FindEntityType("Yavsc.Models.Identity.DeviceDeclaration")?
- .FindProperty("DeclarationDate")?
- .GetDefaultValueSql();
-
- var snapshotDeclDate = snapshotModel
- .FindEntityType("Yavsc.Models.Identity.DeviceDeclaration")?
- .FindProperty("DeclarationDate")?
- .GetDefaultValueSql();
-
- logger.LogWarning(
- "Pending-model diagnostic: DeviceDeclaration.DeclarationDate default SQL runtime='{RuntimeDefaultSql}', snapshot='{SnapshotDefaultSql}'.",
- runtimeDeclDate ?? "(null)",
- snapshotDeclDate ?? "(null)");
-
- bool runtimeHasMusicLoverSettings = db.Model.FindEntityType("Yavsc.Models.Musical.Profiles.MusicLoverSettings") is not null;
- bool snapshotHasMusicLoverSettings = snapshotModel.FindEntityType("Yavsc.Models.Musical.Profiles.MusicLoverSettings") is not null;
- logger.LogWarning(
- "Pending-model diagnostic: MusicLoverSettings runtime={RuntimeHas} snapshot={SnapshotHas}.",
- runtimeHasMusicLoverSettings,
- snapshotHasMusicLoverSettings);
-
- bool runtimeHasSignature = db.Model.FindEntityType("Yavsc.Models.Billing.Signature") is not null;
- bool snapshotHasSignature = snapshotModel.FindEntityType("Yavsc.Models.Billing.Signature") is not null;
- logger.LogWarning(
- "Pending-model diagnostic: Signature runtime={RuntimeHas} snapshot={SnapshotHas}.",
- runtimeHasSignature,
- snapshotHasSignature);
-
- bool runtimeHasModerated = db.Model
- .FindEntityType("Yavsc.Models.Workflow.Activity")?
- .FindProperty("Moderated") is not null;
- bool snapshotHasModerated = snapshotModel
- .FindEntityType("Yavsc.Models.Workflow.Activity")?
- .FindProperty("Moderated") is not null;
- logger.LogWarning(
- "Pending-model diagnostic: Activity.Moderated runtime={RuntimeHas} snapshot={SnapshotHas}.",
- runtimeHasModerated,
- snapshotHasModerated);
- }
- catch (Exception diagEx)
- {
- logger.LogError(diagEx, "Pending-model diagnostic failed.");
- }
- }
-
- private static void EnsureCriticalSchema(IServiceProvider services, ILogger logger)
- {
- try
- {
- var db = services.GetRequiredService();
-
- // Hotfix guard: keep startup resilient if a migration was skipped,
- // while still allowing EF migrations to be the source of truth.
- db.Database.ExecuteSqlRaw(@"
-ALTER TABLE ""Activities""
-ADD COLUMN IF NOT EXISTS ""Moderated"" boolean NOT NULL DEFAULT FALSE;");
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "Critical schema check failed for Activities.Moderated.");
- }
- }
-
private static void SeedConfigurationDatabase(IApplicationBuilder app)
{
try
@@ -1112,26 +980,13 @@ ADD COLUMN IF NOT EXISTS ""Moderated"" boolean NOT NULL DEFAULT FALSE;");
.GetRequiredService()
.CreateScope();
- var logger = app.ApplicationServices
- .GetRequiredService()
- .CreateLogger("Yavsc.Org.Seeding");
-
var configurationDb = scope.ServiceProvider
.GetRequiredService();
var configuration = scope.ServiceProvider
.GetRequiredService();
- logger.LogInformation(
- "Running seed for {DbContext} using provider {Provider}...",
- nameof(IdentityServer8.EntityFramework.DbContexts.ConfigurationDbContext),
- configurationDb.Database.ProviderName ?? "(null)");
-
EnsureDefaultConfiguration(configuration)(configurationDb, true);
-
- logger.LogInformation(
- "Seed completed for {DbContext}.",
- nameof(IdentityServer8.EntityFramework.DbContexts.ConfigurationDbContext));
}
catch (Exception ex)
{
@@ -1141,10 +996,7 @@ ADD COLUMN IF NOT EXISTS ""Moderated"" boolean NOT NULL DEFAULT FALSE;");
var logger = app.ApplicationServices
.GetRequiredService()
.CreateLogger("Yavsc.Org.Seeding");
- logger.LogError(
- ex,
- "Seed failed for {DbContext}.",
- nameof(IdentityServer8.EntityFramework.DbContexts.ConfigurationDbContext));
+ logger.LogError(ex, "ConfigurationDb seeding failed.");
}
}
diff --git a/src/Yavsc.Org/Helpers/EventHelpers.cs b/src/Yavsc.Org/Helpers/EventHelpers.cs
index 151f3360..48c23c1b 100644
--- a/src/Yavsc.Org/Helpers/EventHelpers.cs
+++ b/src/Yavsc.Org/Helpers/EventHelpers.cs
@@ -17,11 +17,11 @@ namespace Yavsc.Helpers
{
Sender = query.ClientId,
Reason = query.Reason,
- Client = new ClientProviderInfo {
+ Client = new ClientProviderInfo {
UserName = query.Client.UserName ,
UserId = query.ClientId,
Avatar = query.Client.Avatar } ,
- Previsional = query.Provisional,
+ Previsional = query.Previsional,
EventDate = query.EventDate,
Location = query.Location,
Id = query.Id,
@@ -44,7 +44,7 @@ namespace Yavsc.Helpers
var yaev = query.CreateEvent("NewHairCutQuery",
string.Format(SR["HairCutQueryValidation"],query.Client.UserName),
$"{query.Client.Id}");
-
+
return yaev;
}
@@ -58,12 +58,12 @@ namespace Yavsc.Helpers
var yaev = new HairCutQueryEvent("newCommand")
{
Sender = query.ClientId,
-
- Client = new ClientProviderInfo {
+
+ Client = new ClientProviderInfo {
UserName = query.Client.UserName ,
UserId = query.ClientId,
Avatar = query.Client.Avatar } ,
- Previsional = query.Provisional,
+ Previsional = query.Previsional,
EventDate = query.EventDate,
Location = query.Location,
Id = query.Id,
diff --git a/src/Yavsc.Org/Helpers/ListItemHelpers.cs b/src/Yavsc.Org/Helpers/ListItemHelpers.cs
index a9101622..4455ee1a 100644
--- a/src/Yavsc.Org/Helpers/ListItemHelpers.cs
+++ b/src/Yavsc.Org/Helpers/ListItemHelpers.cs
@@ -12,16 +12,12 @@ namespace Yavsc.Helpers {
this ApplicationDbContext _dbContext, List activity)
{
var activities = activity.ToArray();
- var activityCodes = activities.Select(a=>a.DoesCode).ToArray();
- var systemActivities = _dbContext.Activities.Where(a=>!a.Moderated
- && activityCodes.Contains(a.Code)).ToArray();
-
- List items = systemActivities.Select(
+ List items = _dbContext.Activities.Select(
x=> new SelectListItem() {
Value = x.Code, Text = x.Name, Selected = activities.Any(a=>a.DoesCode == x.Code)
} ).ToList();
-
+
return items;
}
}
diff --git a/src/Yavsc.Org/Migrations/20260706013420_activityModerated.Designer.cs b/src/Yavsc.Org/Migrations/20260706013420_activityModerated.Designer.cs
deleted file mode 100644
index e76d2d89..00000000
--- a/src/Yavsc.Org/Migrations/20260706013420_activityModerated.Designer.cs
+++ /dev/null
@@ -1,4672 +0,0 @@
-//
-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("20260706013420_activityModerated")]
- partial class activityModerated
- {
- ///
- 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("ClientId1")
- .HasColumnType("integer");
-
- b.Property("GrantType")
- .HasColumnType("text");
-
- b.HasKey("Id");
-
- b.HasIndex("ClientId");
-
- b.HasIndex("ClientId1");
-
- 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("ClientId1")
- .HasColumnType("integer");
-
- b.Property("RedirectUri")
- .HasColumnType("text");
-
- b.HasKey("Id");
-
- b.HasIndex("ClientId");
-
- b.HasIndex("ClientId1");
-
- 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("ClientId1")
- .HasColumnType("integer");
-
- b.Property("Scope")
- .HasColumnType("text");
-
- b.HasKey("Id");
-
- b.HasIndex("ClientId");
-
- b.HasIndex("ClientId1");
-
- 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