Commit graph

124 commits

Author SHA1 Message Date
eebc83cf7e
Add comments API support and tests
Some checks failed
Dotnet build and test / build (push) Has been cancelled
Dotnet build and test / log-the-inputs (push) Has been cancelled
2026-08-10 22:27:52 +01:00
0fd9e40d67
Fix blog comment endpoint path and add regression test
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-08-10 21:48:03 +01:00
44b391d496
Activity protection
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-08-10 18:12:59 +01:00
64547840e4
refacto BlogPost serialization
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled
2026-08-05 21:05:14 +01:00
7d1cca9df0
build
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-08-03 02:16:57 +01:00
786016344b refacto error handling
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-07-12 17:56:23 +01:00
fda43ba2d1 Hsts 2026-07-12 16:27:13 +01:00
13964b9f7f org: show full error details in development
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-07-12 16:07:28 +01:00
b827f531aa Localisation
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
2026-07-12 15:18:07 +01:00
5eb8019beb Audiences 2026-07-12 15:17:53 +01:00
0f73728b85 code format 2026-07-12 15:17:41 +01:00
6e4b68c60d Merge remote-tracking branch 'origin/main' into fic/jwt-validation 2026-07-12 06:12:19 +01:00
713f66f1a7 ? 2026-07-12 04:36:35 +01:00
f3bb039d2f WIP audiences 2026-07-12 02:42:13 +01:00
9846210fd6 ApplicationDbContext: drop redundant HasOne on 3 Client navs
Some checks failed
Dotnet build and test / log-the-inputs (pull_request) Has been cancelled
Dotnet build and test / build (pull_request) Has been cancelled
LoadClientAsync(id) on ClientController used to trip an
IndexOutOfRangeException at the InMemory shaper for any
.Include() of one of three Client navs: RedirectUris,
AllowedScopes, AllowedGrantTypes. Five other Client navs (with
the same EF shape and the same application-level config) worked
fine.

Bisection pointed at the InMemory provider; that hypothesis was
wrong. The real cause is in ApplicationDbContext.OnModelCreating:
yavsc was redeclaring the HasOne<Client>().WithMany(...).
HasForeignKey(e => e.ClientId) for all eight Client* navs. The
same relation is already declared (more completely, with
.IsRequired().OnDelete(DeleteBehavior.Cascade)) by
IdentityServer8's ConfigureClientStore via ModelBuilderExtensions.

The redundant mapping on three specific entities — ClientScope,
ClientRedirectUri, ClientGrantType — interacts with the InMemory
provider's shaper in a way that throws IndexOutOfRange. Removing
the redundancy fixes it.

This commit also walks back b12c272d:
- Drops .AsSplitQuery() from LoadClientAsync (no longer needed
  for the InMemory shaper, and the Postgres path it was a hedge
  against was a false alarm — there is no Postgres production
  bug here, only an InMemory shaper quirk surfaced by the
  redundant mapping).
- Removes the 9 Bisect_*_alone tests that were the artefact of
  the provider-hypothesis phase. They pointed at the right
  entities but for the wrong reason.
- Keeps EditRedirectUris_GET_after_add_lists_both_uris as the
  end-to-end regression sentinel: with the fix in place, it
  loads a Client with two RedirectUris and asserts both are
  rendered. Without the fix, it fails with IndexOutOfRange.
2026-07-12 02:39:13 +01:00
b12c272df7 ClientController: split LoadClientAsync into per-collection subqueries
LoadClientAsync chains 9 .Include() calls on dbContext.Clients.
On Postgres (and InMemory for some IdentityServer8 nav types), the
resulting cartesian product trips the query shaper with
IndexOutOfRangeException at IncludeCollection materialisation time.
Bug reproduces in production on the Blog admin pages that load a
Client by id.

AsSplitQuery() rewrites the load as 9 separate SELECTs joined by
client id, which sidesteps the cartesian explosion and any shaper
ambiguity between Claims/Properties/ClientSecrets (which share
Type/Value column names across some IdentityServer8 versions).

Tests:
- EditRedirectUris_GET_after_add_lists_both_uris: end-to-end
  reproducer that adds a second RedirectUri via POST then re-GETs
  the editor. Guards the fix on the integration path.
- Bisect_*_alone: nine unit tests that exercise the same
  .SingleOrDefaultAsync(c => c.Id == id).Include(nav) on the
  InMemory provider, one nav at a time. Pinpointed three
  problematic navs (RedirectUris, AllowedScopes, AllowedGrantTypes)
  on InMemory; kept as a regression net for any future shaper
  regressions on the InMemory provider (not the Postgres path).
2026-07-12 01:15:02 +01:00
cb20b8a2d5 Revert "repoduces the bug"
This reverts commit fa7794b7a0.
2026-07-11 22:17:58 +01:00
Lum
fa7794b7a0 repoduces the bug 2026-07-11 21:56:25 +01:00
7a066707b3 Tests: route Yavsc.Org test host through Testing environment
TestWebApplicationFactory used ASPNETCORE_ENVIRONMENT=Development, which
caused Program.Main's AddConfiguration("org") to load the tracked
appsettings-org.json (the reference file with the
'*** via dotnet user-secrets ou variable d'environnement ***'
placeholder connection string). Npgsql then failed to parse that
placeholder during host startup, failing six integration tests
(observed 2026-07-11: System.ArgumentException on
NpgsqlConnectionStringBuilder.set_Item).

Switching the test host to a dedicated Testing environment makes
AddConfiguration("org") pick up the new optional
appsettings-org.Testing.json file as the last source in the chain
(JSON → env vars), which overrides YavscConnection with the
InMemory marker and the Smtp section with the test stub values.

The .gitignore exception whitelists this file explicitly: it is a
configuration source for the test host, not a secrets file.

The WebServerFixture path is unchanged — it owns its
WebApplicationBuilder and adds the same in-memory override via its
BuildApp hook.
2026-07-11 20:50:39 +01:00
Lum
bbdcc7f2ad Blog: render user avatar through a null-safe helper
GET /BlogSpot/Details/1 retournait 500 (avec un 500-sur-500 sur
la page d'erreur elle-même) parce que DisplayTemplates
/ApplicationUser.cshtml faisait `var avuri = "/Avatars/" +
Model.UserName + ".s.png"` : avec <Nullable>enable</Nullable>,
Razor émet un null-check implicite sur Model.UserName et lève
NullReferenceException quand l'auteur n'a pas de UserName
posé (donnée héritée, user partiellement initialisé).

- UserDisplayHelpers.AvatarSrc : helper statique pur dans
  Yavsc.Abstract.Identity qui retourne
  YavscConstants.DefaultAvatar pour user null / UserName vide
  ou whitespace, et un path /avatars/<name>.s.png sinon.
- ApplicationUser.cshtml : utilise le helper.
- Tests : 4 cas (null, vide, whitespace, valide) dans
  Yavsc.Org.Tests/NonRegression.

Bonus : le path d'avatar passe de "/Avatars/" (S majuscule,
ne résolvait pas dans le middleware de fichiers statiques) à
YavscConstants.AvatarsPath ("/avatars" minuscule), pour fermer
l'autre trou que centraliser le calcul permettait de fixer
proprement.
2026-07-11 19:41:09 +01:00
Lum
98613e7070 Blog: enforce Restrict FK on BlogPost.Author and Comment.Author
Tout billet a un auteur, tout commentaire a un auteur. On aligne la
base sur ce contrat (Postgres) en droppant les orphelins existants
puis en remplaçant les FK en cascade par des FK Restrict.

- ApplicationDbContext: fluent pour BlogPost.Author et Comment.Author
  en DeleteBehavior.Restrict.
- ApplicationUser: ajoute la nav inverse BlogComments (manquait,
  EF aurait sinon créé une shadow FK).
- Migration 20260711173717_EnforceBlogAuthorFKs: Up purge les
  Comment/BlogSpot dont l'AuthorId n'existe plus, log le volume,
  puis drop+add des FK. Down laisse la cascade (état pré-migration).

Le code applicatif (BlogSpotService.Details) s'appuiera sur cette
contrainte dans un commit séparé.
2026-07-11 18:45:27 +01:00
bed9c8a272 fixes the compile and timestamps to db 2026-07-11 03:26:13 +01:00
375e6482a6 test(yavsc.org): cover the kid derivation in ComputeKid
Extract the kid calculation out of LoadSigningCredentialsInner
into a new internal static HostingExtensions.ComputeKid(string),
and cover it with five focused unit tests in
Yavsc.Org.Tests.ComputeKidTests.

The kid is the bit of signing-credential metadata that ties a
JWT to the right key in the JWKS. Without it, resource servers
(Yavsc.Blogs, Yavsc.Api) fail signature validation with IDX10500
'The signature key was not found', as fixed in 2c6d1157. That fix
inlined three lines of thumbprint-truncation logic at the top of
LoadSigningCredentialsInner, but left the calculation untested.
The tests in this commit pin its shape, value, stability, and
uniqueness, so a future refactor (e.g. switching from SHA-1 to
SHA-256, or moving to X509CertificateLoader for SYSLIB0057) has
to update them deliberately instead of silently changing the
JWKS key id.

Concretely:

  - InternalsVisibleTo("Yavsc.Org.Tests") in AssemblyInfo.cs
    gives the test project access to the new internal method
    without forcing LoadSigningCredentialsInner to leak
    further.
  - ComputeKid(string) is the single source of truth for the
    16-hex truncation; the production call site in
    LoadSigningCredentialsInner now reads
    'var kid = ComputeKid(certPath);'.
  - The inline comment block is updated to say SHA-1 (which is
    what X509Certificate2.GetCertHash() actually returns) instead
    of the previous SHA-256 claim. The behaviour is unchanged.
  - ComputeKid uses X509CertificateLoader.LoadCertificateFromFile
    rather than the obsolete 'new X509Certificate2(string)' ctor
    (SYSLIB0057); same on-disk behaviour, no obsolete warning.

Tests cover:
  - 16-char upper-case hex output matching the first 16 hex
    chars of the cert's GetCertHash();
  - stability across repeated reads of the same cert;
  - distinctness between two independently generated certs;
  - the SHA-1 size of the underlying thumbprint (20 bytes), so
    a future switch to SHA-256 forces a test update;
  - CryptographicException propagation for a missing cert file
    (Assert.ThrowsAny to stay portable across the Linux OpenSSL
    and Windows leaf exception types).
2026-07-09 20:28:58 +01:00
2c6d11577c Yavsc.Org: set KeyId on signing credentials
IdentityServer8 was emitting JWTs without a 'kid' header and
serving the JWKS without per-key identifiers, because
LoadSigningCredentialsInner constructed RsaSecurityKey /
ECDsaSecurityKey objects without an explicit KeyId. Resource
servers (Yavsc.Blogs, Yavsc.Api) cannot match a token to a key
in the JWKS without one, so every signature validation failed
with 'The signature key was not found' (Microsoft.IdentityModel
IDX10500).

Root cause: SigningCredentials were built directly from the
BC-parsed key parameters, bypassing the X509Certificate2 path
IdentityServer normally derives the kid from. The fix derives
a stable KeyId from the certificate's SHA-256 thumbprint
(truncated to 16 hex chars) and sets it on both SecurityKey
variants before constructing SigningCredentials.

The thumbprint-based kid is stable across process restarts as
long as the cert doesn't change, and changes naturally on
LetsEncrypt renewal (~90 days), which is the right behaviour:
old tokens age out, resource servers refresh their JWKS cache
to discover the new kid.

Production rollout: redeploy Yavsc.Org and re-login (or let
the refresh-token path rotate) so newly issued tokens carry
the kid. Pre-restart tokens will continue to be rejected with
IDX10500 until they expire or are refreshed.
2026-07-09 00:32:20 +01:00
89aa2bc37d migration 2026-07-06 03:17:23 +01:00
6ac264fa2c tests 2026-07-06 00:47:35 +01:00
333b066e66 Identity reloaded 2026-07-05 23:56:10 +01:00
ef0f5ddcac refacto FrontmatterParser 2026-07-04 22:58:29 +01:00
7b3a236bcc refacto query status 2026-07-04 22:35:53 +01:00
ee6cb34c23 renaming Reviewed 2026-07-04 22:25:59 +01:00
c74ac71b5d layouts 2026-07-04 20:09:58 +01:00
bce6280750 titres 2026-07-04 20:03:56 +01:00
c8894c4220 gives titles 2026-07-04 19:57:03 +01:00
34b4203cd1 Ui fixes 2026-07-04 19:49:48 +01:00
17838bc78e fixes 2026-07-04 18:46:24 +01:00
a0342ea988 Revert "search all user by email at forgotten password"
This reverts commit 406e2ff03a.
2026-07-04 17:29:49 +01:00
406e2ff03a search all user by email at forgotten password 2026-07-04 17:24:12 +01:00
742da7c3f0 Activity moderated 2026-07-04 17:11:43 +01:00
1d26cbdf3d refacto chathub 2026-07-04 15:28:07 +01:00
ec54108cf0 presentation 2026-07-01 00:13:57 +01:00
0cc82fec5d bug fix IUserSettings 2026-06-30 23:55:43 +01:00
0965caaf40 fixes and renaming 2026-06-30 23:32:05 +01:00
1751145be8 Exploiting GitVersion 2026-06-28 14:11:26 +01:00
ee2c8452ac Navigation and DI 2026-06-26 01:45:21 +01:00
e526b050ed Add the missing Redirect.cshtml view used by LoadingPage helper
AccountController.Signin (and ExternalController / ConsentController)
return this.LoadingPage("Redirect", model.ReturnUrl) when the OIDC
client is a native one (e.g. PostIt, with a custom-scheme redirect
URI). The LoadingPage extension in Yavsc.Extensions renders
controller.View("Redirect", …), so a /Views/Shared/Redirect.cshtml
must exist.

The file was missing, and the absence surfaced as a 500 on
POST /signin once the login itself succeeded — the user authenticated
fine, Identity.Application signed in, but the response body never
rendered and the POST returned InvalidOperationException
('The view Redirect was not found'). This is what broke the PostIt
flow after the seed/IdentityResource fixes landed.

The view is the standard IdentityServer quickstart loading page: a
meta-refresh that redirects the embedded browser to the OIDC
client's callback URI (postit://callback). Localizer strings are
used so the page is translatable like the rest of the auth UI.
2026-06-26 00:21:58 +01:00
847557318f code cleanup 2026-06-25 23:55:52 +01:00
571977f81b Fix LINQ translation in EnsureDefaultApplicationScopes
EF Core was throwing at startup with:

  System.InvalidOperationException: The LINQ expression
  '[ApiResourceScopeSpecification,...].Any(s => s.ResourceName == r.Name)'
  could not be translated.

The cause: Constants.ApiResourcesScopes is a static readonly C# array,
not an IQueryable, but it was used directly inside a Where clause on an
IQueryable<ApiResource>. EF tried to translate the closure over
Constants.ApiResourcesScopes into a SQL sub-query, which is not a
supported operation.

Materialise the wanted resource names into a HashSet before letting EF
see the Where — the collection is small (5 entries) so there's no
performance reason to push it down. After this fix,
EnsureDefaultApplicationScopes runs to completion at startup and
the seed actually has a chance of doing its job (assuming the rows
aren't already present).
2026-06-25 23:20:43 +01:00
68eb24ba44 Don't seed openid/profile/offline_access as ApiScopes
IdentityServer8 refuses to start when an IdentityResource and an
ApiScope share the same Name — it throws

  Found identity scopes and API scopes that use the same names.
  This is an invalid configuration. Scopes found: openid, profile

and the host crashes before serving any request.

Constants.BuildInApiScopes has historically listed 'openid',
'profile' and 'offline_access' alongside the application scopes
(admin, moderation, performer, client). The IdentityResource
counterparts are seeded separately via
IdentityResources.OpenId().ToEntity() /
IdentityResources.Profile().ToEntity() in
EnsureDefaultApplicationScopes, so listing them again in
BuildInApiScopes produces a duplicate 'openid' / 'profile' once
that seeder is wired into MigrateDatabase and starts running on
every restart (commit be334a69). 'offline_access' is handled
directly by IdentityServer8 (DefaultResourceValidator has a
special-case branch for it) and never needs an ApiScope row.

Trim BuildInApiScopes to application scopes only. The live
ConfigurationDb already contains both IdentityResources and
ApiScopes for the same names from earlier hand-rolled SQL
bootstrap, so the duplicate-name check fires the moment the
process tries to enumerate its resources at startup.
2026-06-25 23:11:16 +01:00
bbfaa039de Set explicit defaults on seeded ApiScopes and ApiResources
The previous commit (be334a69) relied on C# defaults to populate
the Postgres NOT NULL columns Enabled, Required, Emphasize,
ShowInDiscoveryDocument (on ApiScopes) and Created (on
ApiResources). Both tables declare these columns NOT NULL without
a database default, so EF Core ends up shipping C# defaults
(false / DateTime.MinValue) that violate the constraints or
silently disable the seeded rows.

Concretely, if we deployed be334a69 as-is:

- ApiScopes.Enabled = false -> the scope is invisible to
  DefaultResourceValidator, exactly the bug we're fixing.
- ApiResources.Created = DateTime.MinValue (0001-01-01) ->
  Postgres rejects the INSERT with
  'null value in column Created violates not-null constraint'.

Set the values explicitly so the seeder produces the same state
whether it runs once or a hundred times, fresh database or not.
2026-06-25 21:30:56 +01:00
be334a69dc Seed ApiResources + ApiResourceScopes, run seeder on every startup
The previous commit (37440171) added ApiScope rows for the
application scopes (admin, moderation, performer, client, blogs).
It was a partial fix: an ApiScope alone is not a valid scope from
DefaultResourceValidator's point of view. The validator only
recognises a scope if it can find an ApiResource that exposes it
(via ApiResourceScopes). Without that link, /connect/authorize
rejects the request with 'Scope X not found in store', even
though the scope row exists. This is what killed the PostIt login
in production.

This commit:

1. Extends Constants.ApiResourcesScopes with ResourceName +
   ResourceDisplayName. Topology: one ApiResource per scope
   ('admin' resource exposes 'admin' scope, 'blogs' resource
   exposes 'blogs' scope, etc.) — keeps each scope's audience
   specific if/when we split products across separate audiences.

2. Ensures EnsureDefaultApplicationScopes also inserts the
   matching ApiResource rows (deduped on Name) and ApiResourceScope
   rows linking each resource to its scope. Idempotent: missing
   rows are added, nothing is removed.

3. Removes the b.UseSeeding(...) call inside AddConfigurationStore.
   EF Core's UseSeeding callback only fires when the database is
   empty, so on a live ConfigurationDb (which already had Clients
   and ClientScopes) it never ran — that is why the previous commit
   had no visible effect on production. The seeder is now invoked
   explicitly from MigrateDatabase via SeedConfigurationDatabase,
   which resolves ConfigurationDbContext from the DI and runs
   EnsureDefaultConfiguration on every startup, regardless of
   whether the database was fresh.

   Seeding failures are caught and logged (best-effort) so a
   misconfigured seeder cannot prevent the host from booting.

Live data on yavsc.pschneider.fr is still missing the
ApiResource/ApiResourceScope rows; a one-shot SQL or a redeploy
with this commit is needed before PostIt can log in. Production
fix to follow.
2026-06-25 21:28:13 +01:00