Commit graph

25 commits

Author SHA1 Message Date
99f4361e2d Api Resources seed 2026-06-25 20:22:41 +01:00
f3a3b63595 WIP PostIt login 2026-06-25 00:08:25 +01:00
2dec799d71 Introduce Yavsc.Interfaces.ISmtpClient and a recording test fake
Narrow ISmtpClient to the four operations MailSender actually uses,
behind a Yavsc.Interfaces.ISmtpClientFactory. Production wires
MailKitSmtpClient (SmtpClientFactory); tests wire a recording fake
(RecordingSmtpClientFactory). The fake is pre-registered in
WebServerFixture so SMTP calls are short-circuited; the EMailling
test now asserts the Connect -> Authenticate -> Send -> Disconnect
sequence. Yavsc.Abstract stays free of MailKit/MimeKit.
2026-06-22 02:09:02 +01:00
6aaff74082 fix(client-controller): single constructor with IHtmlLocalizer
The partial class ClientController had two constructors declared
across ClientController.cs and ClientController.Collections.cs.
ASP.NET Core DI failed to pick one at request time with:

  System.InvalidOperationException: Multiple constructors accepting
  all given argument types have been found in type
  'Yavsc.Controllers.ClientController'.

Move IHtmlLocalizer<ClientController> into the primary constructor
in ClientController.cs and drop the duplicate one in
ClientController.Collections.cs. The Collections partial now keeps
only its readonly field and action methods; the constructor and
field assignment are unified on the main file.

Also add the missing 'using Microsoft.AspNetCore.Mvc.Localization;'
to ClientController.cs so IHtmlLocalizer resolves.
2026-06-21 21:14:20 +01:00
d62e59ba30 postit: trailing-slash regression + loopback browser cleanup; identityserver: BC cert loader + SecurityKey
PostIt
- LoginPage renders StatusMessage as a read-only TextBox so the
  text is mouse-selectable and copyable (no copy button).
- LoginPageViewModel exposes ExternalUrl (Authentication.Authority
  with trailing slash stripped) and DiscoveryUrl
  (ExternalUrl + '/.well-known/openid-configuration'). LoginAsync
  surfaces the discovery URL before the call and suffixes it onto
  every error message, so reachability issues are diagnosable by
  pasting the URL into a browser.
- LoopbackBrowser (used for the OIDC redirect listener on desktop)
  now bounds the GetContextAsync wait at 5 minutes and calls both
  Stop() and Close() in the finally, so the listener is always
  released even if the user abandons the flow. Without this, the
  next PostIt launch fails with 'Failed to listen on prefix
  http://127.0.0.1:7890/ because it conflicts with an existing
  registration on the machine.'
- Add LoginPageViewModelTests.LoginAsync_works_when_authority_has_trailing_slash
  to lock in the discovery URL contract.
- Add LoopbackBrowserTests covering both timeout and happy-path
  listener cleanup.

Yavsc.Org
- Drop the CustomEntries['jwks_uri'] override from commit 84160f07.
  IdentityServer8 reserves that key and rejects the override with
  'Discovery custom entry jwks_uri cannot be added, because it
  already exists.' The default /.well-known/openid-configuration/jwks
  endpoint is what DiscoveryKeyEndpoint actually serves.
- Replace X509Certificate2.CreateFromPemFile + the 3-arg
  X509Certificate2(path, key, X509KeyStorageFlags) ctor with a
  BouncyCastle-backed loader. The BCL path raised
  InvalidOperationException during AddSigningCredential and aborted
  the runtime with SIGABRT (code=6/ABRT, libcoreclr.so stack) on
  the production EC Let's Encrypt cert. BouncyCastle 2.6.2
  PemReader accepts PKCS#1 + PKCS#8 EC/RSA PEMs uniformly; RSA
  path uses DotNetUtilities.ToRSA, EC path rebuilds ECDsa from
  ECParameters with the curve dispatched by NIST order bit length
  (256/384/521).
- Switch the signing credential handed to IdentityServer8 from
  X509Certificate2 to a SigningCredentials built from a SecurityKey
  (RsaSecurityKey / ECDsaSecurityKey). The cert loaded fine but
  IdentityServer8's key material service reads cert.PrivateKey at
  runtime — on Linux that handle is not retained across the
  X509Certificate2 / runtime boundary, so CreateJwkDocumentAsync
  raised NullReferenceException on the first GET /jwks. The
  SecurityKey is a pure managed object whose Key is a live
  AsymmetricAlgorithm, which survives every read IdentityServer
  does.
- Add BouncyCastle.Cryptography 2.6.2 to src/Yavsc.Org/Yavsc.Org.csproj
  and the matching PackageVersion in Directory.Packages props.
- Wrap the loader in a try/catch that prints the full managed
  stack to stderr on failure, so future PEM-format issues surface
  in journalctl instead of being hidden behind SIGABRT.
2026-06-21 07:36:22 +01:00
84160f0759 login status and histing extentions 2026-06-21 03:55:32 +01:00
7a0944d0f5 PostIt.Desktop: wire the loopback browser, parameterise PostIt RedirectUris
The previous commit set Platform.CreateBrowser to null on the desktop
side, so LoginAsync would still fail with 'No browser is available'.
Close that loop with an explicit desktop bootstrap.

PostIt.Desktop/PlatformBootstrap.cs mirrors the Android side: it
populates Platform.DefaultRedirectUri and Platform.CreateBrowser
once at startup. Program.Main calls EnsureInitialized before
BuildAvaloniaApp so the LoginPageViewModel sees a working browser
before any login attempt.

The Yavsc.Org seed now reads Site:ExternalUrl from configuration so
the RedirectUri list for the PostIt client follows the same setting
as the rest of the application (same value used in
Administration/ClientController, AccountController, etc.). Without
this, an embedded 'launch PostIt from a Yavsc.Org page' scenario
would be rejected by IdentityServer (redirect_uri mismatch).

BuildPostItRedirectUris is a small helper that yields the constant
PostItRedirectUris (loopback + Android custom scheme) followed by
Site:ExternalUrl when set. Both SeedNewPostItClient (fresh db) and
MigratePostItClientToPublic (existing db) consume it. The legacy
cleanup block (which used to remove https://yavsc.pschneider.fr/
and yavsc://callback) is dropped: Site:ExternalUrl is now the
canonical way to authorise that path and may legitimately equal
that value.
2026-06-20 17:49:53 +01:00
512a0ef06f PostIt: switch OIDC client from confidential (client_secret) to public (PKCE/JWT)
PostIt is a desktop/mobile app talking to Yavsc.Org
(https://yavsc.pschneider.fr) as an OIDC identity provider. The
previous grant used the client_credentials flow with a client_secret
embedded in postit-settings.json: this was both insecure (secret
travels with the binary) and inappropriate for an interactive app
(token had no user identity, so the API could not scope or audit).

The new flow is Authorization Code + PKCE:

  * PostIt client (Settings/AuthenticationSettings.cs): the
    ClientSecret property is removed; GetOidcClientOptions now drops
    the secret and accepts an optional IBrowser supplied per-platform.
  * Settings.cs: new AndroidRedirectUri constant ('android://postit-signin')
    that the Android app uses; RedirectUri is no longer hard-coded in
    MainViewModel.
  * MainViewModel.cs: the manual discovery + client_credentials POST is
    replaced with OidcClient.LoginAsync (Authorization Code + PKCE).
  * Settings sample: Authority points at the real Yavsc.Org OP, not at
    a non-existent Keycloak-style realm path.
  * Yavsc.Org/Extensions/HostingExtensions.cs: the 'postit' client seed
    is now idempotent (MigratePostItClientToPublic) and detects
    legacy state on existing ConfigurationDb rows - flips
    RequireClientSecret=false, RequirePkce=true, drops any ClientSecret
    row, and replaces the legacy RedirectUris
    (https://yavsc.pschneider.fr/, yavsc://callback) with the current
    set (http://127.0.0.1:7890/, android://postit-signin).

PostIt.Android:

  * MainActivity: explicit Name attribute so the activity alias can
    target a stable component; LaunchMode.SingleTask so the existing
    instance receives the deep-link Intent; OnNewIntent forwards the
    callback URI through AndroidOidcCallbackSink.
  * AndroidManifest.xml: activity-alias PostIt.Android.OidcCallbackActivity
    exposing scheme=android host=postit-signin to Android, so the OP
    redirect lands back in the running PostIt instance.

The IdentityModel.OidcClient.Browser.SystemBrowser package and a
thin AndroidSystemBrowser implementation are added in a follow-up so
OidcClient.LoginAsync can actually drive Chrome Custom Tabs and
consume AndroidOidcCallbackSink.
2026-06-20 17:16:07 +01:00
dcf2a93ad0 Split Site:Audience into Site:ExternalUrl + Site:CorsAllowedOrigins
The Site:Audience setting was conflating two distinct concepts: an OAuth
JWT audience (a single resource identifier) and a CORS allow-list (an
array of origins). Collapsing them caused several latent bugs:
- OAuth/JWT validation expected a single string while CORS WithOrigins
  accepts an array.
- Password-reset callback URLs and OAuth client RedirectUri/Origin were
  being built from what was meant to be an audience identifier, not a
  base URL.
- Yavsc.Org's main CORS policy was hardcoded to '*', with no way to
  restrict it without code changes.

Changes:
- SiteSettings.Audience (string) replaced with CorsAllowedOrigins
  (IList<string>).
- OAuth JWT Authority still reads Site:Authority; Audience now reads
  Site:ExternalUrl (Org only; Api/Blogs use ValidateAudience=false).
- MailSender and AccountController build reset-callback URLs from
  Site:ExternalUrl.
- ClientController uses Site:ExternalUrl for OAuth RedirectUri/Origin
  defaults on newly created clients.
- Yavsc.Api and Yavsc.Blogs now read CORS origins from
  Site:CorsAllowedOrigins instead of hardcoded URLs.

Add shared AddYavscCors / AddYavscJwtBearer extension methods in
Yavsc.Server/Helpers/ServiceExtensions.cs to enforce a single
configuration contract across all runtime services (Api, Blogs, Org).
Fails closed when CorsAllowedOrigins is empty; fails fast at startup
when Site:Authority is missing.

Remove obsolete ConfigurationHelpers.GetAudience (no remaining callers).

Local appsettings-*.json files (which carry deployment-specific values
and are gitignored) must be updated to add Site:CorsAllowedOrigins.
2026-06-19 13:15:21 +01:00
c1f4d19975 fices the UI 2026-06-15 02:55:22 +01:00
ec5c1b6a95 fix(cookies): set Identity cookies to SameSite=Lax in dev (avoid Chromium rejection on http://localhost)
Chromium rejects cookies that have SameSite=None but no
Secure flag. The default Identity cookie policy uses
SameSite=None, which is invalid on http://localhost (no
TLS, no Secure). Result on http://localhost:5000:

  Cookie '.AspNetCore.Identity.Application' rejected
  because it has the 'SameSite=None' attribute but is
  missing the 'secure' attribute.

Fix: in Development environment, configure
ConfigureApplicationCookie and ConfigureExternalCookie
to use SameSite=Lax and SameAsRequest SecurePolicy.
Lax is permissive enough for OAuth callbacks (top-level
GET navigations) and avoids the rejection.

Production (https://) is untouched — the default
SameSite=None is correct when Secure is set.

Note on the sameSiteMode reference: SameSiteMode is
defined in two namespaces
(Microsoft.AspNetCore.Http and Microsoft.Net.Http.Headers).
The file already uses 'using Microsoft.Net.Http.Headers;'
so a bare 'SameSiteMode' is ambiguous. Using the
fully-qualified name 'Microsoft.AspNetCore.Http.SameSiteMode'
to disambiguate, no new using needed.

Tested: dotnet build OK, dotnet test 11/11 green.
2026-06-14 16:27:54 +01:00
1a0556695c Login settings 2026-06-10 16:59:23 +01:00
164bd928aa logo + oidc client PostIt 2026-06-10 11:10:15 +01:00
57adfc82a5 Start protecting the Api 2026-06-06 21:30:41 +01:00
e24208e77b refact 2026-05-30 19:34:22 +01:00
95dec3636a Use InMemory db driver at testing 2026-05-28 22:18:26 +01:00
8a46341755 a scoring model 2026-05-24 19:35:35 +01:00
6cc0c519d2 tests OK 2026-04-20 00:35:51 +01:00
65db349c0a fixes the current date at testing phase 2026-04-19 19:17:34 +01:00
d21337d4a6 about to test remote access 2026-04-19 16:02:50 +01:00
87d62791b8 fix: test infrastructure with in-memory DB, SMTP mocking, and thread-safe billing configuration
- Add in-memory database support for test isolation in WebServerFixture
- Implement TestMailSender fake SMTP provider for email test support
- Add thread synchronization to billing service registration to prevent race conditions
- Make RegisterBilling<T> idempotent to safely handle reconfiguration
- Configure test environment via in-memory settings (UseTestEmailSender, UseInMemoryDatabase)
- Add regression tests for billing module idempotency and duplicate registration detection
- Fix tests: EMaillingTests.SendEMailSynchrone, BillingServiceTests (2 tests), HaveConfigurationRoot (3 tests)

All core test infrastructure tests now passing.
2026-04-19 14:40:40 +01:00
Paul Schneider
be7df3d054 APi Scopes 2026-03-16 23:16:12 +00:00
Paul Schneider
e042e34bf7 refact and login 2026-03-09 02:07:42 +00:00
Paul Schneider
acc34aeee4 cleanup db migrations 2026-03-01 20:14:54 +00:00
Paul Schneider
40e8e08690 reorg 2026-02-28 21:17:54 +00:00
Renamed from src/Org/Extensions/HostingExtensions.cs (Browse further)