refinement phase 5

This commit is contained in:
hamid
2026-07-13 16:00:34 +03:30
parent 64f6aa45c9
commit d4147342da
16 changed files with 440 additions and 79 deletions
+34 -17
View File
@@ -56,9 +56,14 @@ You are a **senior .NET software engineer** working on this codebase. That means
| Update DB | `dotnet ef database update --project src/Infrastructure/Baya.Infrastructure.Persistence --startup-project src/API/Baya.Web.Api` |
**Default URL:** `https://localhost:5002` — Swagger at `/swagger`.
On boot, `Program.cs` calls `ApplyMigrationsAsync()`, `SeedDefaultUsersAsync()`, `SeedPaymentGatewaysAsync()`
— and, **only in Development**, `SeedDemoWorldAsync()` (the demo marketplace seeder, see Persistence below).
A reachable SQL Server is required to start.
On boot (non-Testing), `Program.cs` calls `ApplyMigrationsAsync()` + `SeedDefaultUsersAsync()` (roles always;
a bootstrap admin **only if `Seed:AdminUsername`/`Seed:AdminPassword` are configured** — never a committed
credential), and **only in Development** `SeedPaymentGatewaysAsync()` (the sandbox gateway) + `SeedDemoWorldAsync()`
(the demo marketplace, see Persistence below). A reachable SQL Server is required to start. Startup **fails fast**
(`StartupSecretsGuard`) if a load-bearing secret — the DB connection strings, and in deployed environments the
JWE + field-encryption keys — is missing or left at its committed `SET_VIA_USER_SECRETS_OR_ENV` placeholder
(refinement-phase-5). Development supplies working dev-only crypto keys via `appsettings.Development.json`; only
the connection string must come from user-secrets (see [RUNBOOK](../dev/post-phase/refinement/RUNBOOK.md)).
---
@@ -510,25 +515,29 @@ only canonical if it stays accurate.
Service registration is composed from per-layer extension methods (each project's `ServiceConfiguration/`):
```
builder.ValidateRequiredSecrets() // refinement-phase-5: fail fast on missing/placeholder DB + crypto secrets
ConfigureHealthChecks() · SetupOpenTelemetry()
AddApplicationServices() // Mediator + pipeline behaviors (Logging → Metrics → Validate)
RegisterIdentityServices(...) // Identity, JWT/JWE, authorization policies, ICurrentUser + IHttpContextAccessor
RegisterIdentityServices(…, requireHttpsMetadata) // Identity, JWT/JWE (RequireHttpsMetadata on outside Dev/Testing), ICurrentUser
AddPersistenceServices(...) // DbContext (+ AuditFieldInterceptor), UnitOfWork, repositories
AddCrossCuttingSeams(config) // IDateTimeProvider, IFieldEncryptor, ICacheService, IObjectStorage, INotificationDispatcher (mocks)
AddWebFrameworkServices() // API versioning + snake_case routing
AddCorsPolicies(config) // browser CORS policy from Cors:AllowedOrigins (refinement-phase-0; default http://localhost:3000 in Dev)
AddRateLimitingPolicies() // built-in rate limiter: per-IP global + named (otp/auth/sensitive)
AddForwardedHeadersConfiguration(config) // refinement-phase-5: trust ForwardedHeaders:KnownProxies/KnownNetworks so the rate limiter sees the real client IP behind a proxy
AddRateLimitingPolicies() // built-in rate limiter: per-resolved-IP global + named (otp/auth/sensitive/webhook)
AddSwagger("v1", "v1.1") · RegisterValidatorsAsServices() · AddMapster()
ConfigureGrpcPluginServices()
// Development-only: AddDevelopmentOtpCapture() (refinement-phase-0) decorates ISmsSender to capture each
// OTP in-memory for the GET /api/v1/dev/last_otp/{phone} helper — never wired outside Development.
```
Pipeline order: exception handler → Swagger → routing → **CORS → rate limiter → authentication →
authorization** → controllers → metrics → health checks → gRPC. `UseCors(...)` (refinement-phase-0) sits
**after `UseRouting()` and before `UseRateLimiter()`** so a pre-flight `OPTIONS` is answered before the
limiter/auth run; `UseRateLimiter()` is placed **before** `UseAuthentication()` so over-limit auth/OTP
attempts are rejected (`429`) before hitting the auth stack.
Pipeline order: **forwarded headers** → exception handler → Swagger → routing → **CORS → rate limiter →
authentication → authorization** → controllers → metrics → health checks → gRPC. `UseForwardedHeaders()`
(refinement-phase-5) is **first** so the resolved client IP (`X-Forwarded-For` from a trusted proxy) is in
place before the rate limiter partitions on it. `UseCors(...)` (refinement-phase-0) sits **after
`UseRouting()` and before `UseRateLimiter()`** so a pre-flight `OPTIONS` is answered before the limiter/auth
run; `UseRateLimiter()` is placed **before** `UseAuthentication()` so over-limit auth/OTP attempts are
rejected (`429`) before hitting the auth stack.
When adding new infrastructure, expose it as an extension method and call it from `Program.cs` —
never inline registrations there directly.
@@ -606,18 +615,26 @@ action to `sender.Send(...)`. Full conventions are in [CONVENTIONS.md](CONVENTIO
process-wide singleton because EF caches the model). Equality lookups go through the deterministic
`PhoneHash` column (UNIQUE, synced on SaveChanges — which also resets `ShahkarVerifiedAt` when the
phone actually changes). Never query `PhoneNumber == x`.
- **Roles:** full vocabulary in `Domain/Entities/User/RoleNames` (seeded by `SeedDataBase`).
`customer`/`nurse` are self-selectable via `POST me/select_role` (audited
`granted_by`/`granted_at`, idempotent, both can be held); admin sub-roles are internal-only and
return 403 there. `user_roles.revoked_at` has a global query filter, so revoked grants disappear
from every role read automatically. Auth knobs (`auth_otp_resend_seconds`, `auth_otp_max_attempts`,
- **Roles:** full vocabulary in `Domain/Entities/User/RoleNames`; `SeedDataBase` always seeds the roles,
and seeds a **bootstrap admin only when `Seed:AdminUsername`/`Seed:AdminPassword` are configured**
(refinement-phase-5 — no more committed `admin`/`qw123321`; break-glass only, day-to-day admins come from
the phone-OTP demo seeds or are provisioned out-of-band). `customer`/`nurse` are self-selectable via
`POST me/select_role` (audited `granted_by`/`granted_at`, idempotent, both can be held); admin sub-roles are
internal-only and return 403 there. `user_roles.revoked_at` has a global query filter, so revoked grants
disappear from every role read automatically. Auth knobs (`auth_otp_resend_seconds`, `auth_otp_max_attempts`,
`auth_session_ttl_days`) are `platform_configs` rows read via `IPlatformConfig`.
- Dynamic permission system: `DynamicPermissionHandler` reads `[controller]` + `[action]` route
values and checks role claims. Always use `[controller]`/`[action]` tokens so the keys stay
consistent (see CONVENTIONS.md §1 Routing).
- Settings bound from `appsettings.json` → `IdentitySettings`.
- Settings bound from `appsettings.json` → `IdentitySettings`. **JWE keys are never committed**: the
committed values are `SET_VIA_USER_SECRETS_OR_ENV` placeholders (real ones via user-secrets/env; Development
uses dev-only keys in `appsettings.Development.json`). `RequireHttpsMetadata` is **on outside Dev/Testing**
(passed into `RegisterIdentityServices`), the access-token lifetime is `ExpirationMinutes: 60`, and
`Issuer`/`Audience` are real (`Balinyaar`/`BalinyaarClient`) — refinement-phase-5.
- Auth and OTP endpoints must be rate-limited (CONVENTIONS.md §11) — `request_otp`/`verify_otp` use
the `otp` policy, `refresh` the `auth` policy; plus a per-phone resend window via `ICacheService`.
the `otp` policy, `refresh` the `auth` policy; plus a per-phone resend window via `ICacheService`. The two
PSP/BNPL webhooks share the single deliberate **`webhook`** policy (bursty-tolerant, partitioned per-provider);
behind a reverse proxy the limiter partitions on the forwarded client IP (see Startup wiring).
---