refinement phase 5
This commit is contained in:
+34
-17
@@ -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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user