backend phase 0: foundation, cross-cutting seams & starter cleanup
Remove the Order demo (entity/feature/repo/config/gRPC/proto) and the three pre-marketplace migrations; regenerate a fresh InitialBaseline migration. Stand up the REST surface (PingController + System/Ping CQRS) proving the Mediator -> behaviors -> OperationResult -> ApiResult envelope end to end. Close wiring gaps: register LoggingBehavior (outermost) and add the built-in rate limiter (per-IP global + otp/auth/sensitive policies), placed before authentication. Add current-user + audit plumbing: ICurrentUser (HttpContext + null impls), rename BaseEntity audit fields to CreatedAt/ModifiedAt (DateTimeOffset) + CreatedById/ModifiedById, stamped by a new AuditFieldInterceptor. Introduce five cross-cutting seams (IDateTimeProvider, IFieldEncryptor, ICacheService, IObjectStorage, INotificationDispatcher) with in-memory/local mocks registered via AddCrossCuttingSeams. Add Baya.Test.Foundation (encryptor, audit interceptor, ping handler) and update docs, contracts (swagger.v1.json), handoff, report, and mocks registry.
This commit is contained in:
@@ -0,0 +1,256 @@
|
|||||||
|
{
|
||||||
|
"x-generator": "NSwag v14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))",
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"info": {
|
||||||
|
"title": "Clean Architecture OpenAPI docs",
|
||||||
|
"version": "v1"
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "http://127.0.0.1:5082"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"/api/v1/ping/get_status": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Ping"
|
||||||
|
],
|
||||||
|
"operationId": "Ping_GetStatus",
|
||||||
|
"responses": {
|
||||||
|
"400": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ApiResult"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ApiResult"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ApiResult"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ApiResultOfPingQueryResult"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/ping/get_status_rate_limited": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Ping"
|
||||||
|
],
|
||||||
|
"operationId": "Ping_GetStatusRateLimited",
|
||||||
|
"responses": {
|
||||||
|
"400": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ApiResult"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ApiResult"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ApiResult"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ApiResultOfPingQueryResult"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"ApiResultOfDictionaryOfStringAndListOfString": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/ApiResult"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"nullable": true,
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ApiResult": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"isSuccess": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"statusCode": {
|
||||||
|
"$ref": "#/components/schemas/ApiResultStatusCode"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"requestId": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ApiResultStatusCode": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "",
|
||||||
|
"x-enumNames": [
|
||||||
|
"Success",
|
||||||
|
"BadRequest",
|
||||||
|
"UnAuthorized",
|
||||||
|
"Forbidden",
|
||||||
|
"NotFound",
|
||||||
|
"NotAcceptable",
|
||||||
|
"EntityProcessError",
|
||||||
|
"FailedDependency",
|
||||||
|
"ServerError"
|
||||||
|
],
|
||||||
|
"enum": [
|
||||||
|
200,
|
||||||
|
400,
|
||||||
|
401,
|
||||||
|
403,
|
||||||
|
404,
|
||||||
|
406,
|
||||||
|
422,
|
||||||
|
424,
|
||||||
|
500
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ApiResultOfPingQueryResult": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/ApiResult"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"nullable": true,
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/PingQueryResult"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"PingQueryResult": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"service": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"serverTimeUtc": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"securitySchemes": {
|
||||||
|
"Bearer": {
|
||||||
|
"type": "http",
|
||||||
|
"description": "Enter JWT Token ONLY",
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"scheme": "Bearer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,4 +12,16 @@ One block per completed backend phase. Newest at the top. Backend lane writes he
|
|||||||
- **Notes for frontend:** <anything load-bearing>
|
- **Notes for frontend:** <anything load-bearing>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
_(no phases completed yet)_
|
## backend-phase-0 — Foundation, cross-cutting seams & starter cleanup — 2026-06-28
|
||||||
|
- **Shipped:** removed the `Order` demo (entity/feature/repo/config/gRPC) + 3 old migrations; fresh
|
||||||
|
`InitialBaseline` migration; REST surface (`PingController` + `System/Ping` CQRS); `ICurrentUser` +
|
||||||
|
`AuditFieldInterceptor`; five cross-cutting seams (`IDateTimeProvider`, `IFieldEncryptor`,
|
||||||
|
`ICacheService`, `IObjectStorage`, `INotificationDispatcher`) with mocks; `LoggingBehavior` +
|
||||||
|
rate limiter (per-IP global + `otp`/`auth`/`sensitive`).
|
||||||
|
- **Contracts:** `dev/contracts/openapi/swagger.v1.json` published (envelope + ping schemas).
|
||||||
|
- **Mocked:** the 5 seams above → 🟡 (see reports/mocks-registry.md).
|
||||||
|
- **Gate:** build clean (0 new warnings) / tests green (10 pass). Live API verified vs `192.168.100.14`
|
||||||
|
(migration applied + seeded; ping `200`; rate-limit `429`).
|
||||||
|
- **Handoff:** backend/handoff/after-backend-phase-0.md
|
||||||
|
- **Notes for frontend:** `ApiResult` envelope is fixed (camelCase body, snake_case URLs);
|
||||||
|
`GET /api/v1/ping/get_status` is live to wire types against; `429` on over-limit.
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# After backend-phase-0 — what f0/b1 can rely on
|
||||||
|
|
||||||
|
**The spine is clean and REST works.** The inherited `Order` demo (entity/feature/repo/config/gRPC) and
|
||||||
|
the three pre-marketplace migrations are gone; Identity + JWE auth + dynamic permissions + the CQRS
|
||||||
|
behaviors + observability are untouched and the existing tests stay green. A real REST controller is
|
||||||
|
live and proves the full pipeline.
|
||||||
|
|
||||||
|
## What the frontend (f0) can rely on now
|
||||||
|
- **The response envelope is fixed.** Every endpoint returns the server's `ApiResult` envelope, not a
|
||||||
|
bare body. Success shape:
|
||||||
|
```json
|
||||||
|
{ "isSuccess": true, "statusCode": 0, "message": "Success", "requestId": "<trace>", "data": <T> }
|
||||||
|
```
|
||||||
|
Failure returns `400/401/403/404` with the same envelope (field-level errors for validation). This is
|
||||||
|
what `clientFetch`/`serverFetch` must unwrap. JSON casing is the server default (camelCase for the
|
||||||
|
envelope/body properties; **URL segments are snake_case**).
|
||||||
|
- **A working endpoint to wire the type pipeline against:**
|
||||||
|
`GET /api/v1/ping/get_status` → `ApiResult<{ service, status, serverTimeUtc }>`.
|
||||||
|
- **Rate limiting exists.** Over-limit requests get `429`; `get_status_rate_limited` demonstrates it
|
||||||
|
(first 5/10s OK, then 429).
|
||||||
|
- **swagger.json is published:** `dev/contracts/openapi/swagger.v1.json` (envelope schemas
|
||||||
|
`ApiResult`, `ApiResultStatusCode`, `PingQueryResult`). Generate frontend types from it.
|
||||||
|
|
||||||
|
## What b1 can rely on now
|
||||||
|
- **Audit base type:** `BaseEntity`/`IAuditableEntity` (`Domain/Common/BaseEntity.cs`) with
|
||||||
|
`CreatedAt`/`ModifiedAt` (`DateTimeOffset`) + `CreatedById`/`ModifiedById` (`int?`). New entities that
|
||||||
|
derive `BaseEntity` get audit stamping for free.
|
||||||
|
- **Audit interceptor:** `AuditFieldInterceptor` (`Infrastructure.Persistence/Interceptors/`) stamps
|
||||||
|
those fields on save from `ICurrentUser` + `IDateTimeProvider`. **b1 extends this** to also write the
|
||||||
|
append-only `audit_logs` rows — the stamping plumbing and the clean extension point are in place.
|
||||||
|
- **Cross-cutting seams (DI-registered, mocked):** `IDateTimeProvider`, `IFieldEncryptor` (use it for
|
||||||
|
every encrypted PII column — phone/national_id/IBAN/addresses/clinical notes; deterministic `Hash`
|
||||||
|
for `*_hash` lookup columns), `ICacheService` (cache `platform_configs` / read-heavy data through it),
|
||||||
|
`IObjectStorage`, `INotificationDispatcher`. Contracts in `Application/Contracts/Common`; mocks in
|
||||||
|
`Infrastructure.CrossCutting/Seams/`; registered by `AddCrossCuttingSeams`.
|
||||||
|
- **Money rule:** IRR is `long`/`BIGINT`, integer-only, no floats (CONVENTIONS §6).
|
||||||
|
- **Migration baseline:** `20260628191947_InitialBaseline`. Add the marketplace schema on top of it;
|
||||||
|
generate with the documented `dotnet ef migrations add` command.
|
||||||
|
- **Rate-limit policies** `otp`/`auth`/`sensitive` are defined and ready to apply via
|
||||||
|
`[EnableRateLimiting("...")]` on the relevant endpoints (b2 auth/OTP).
|
||||||
|
|
||||||
|
## Known caveat
|
||||||
|
Non-Development environments route Serilog to the `logDb` connection (`Server=sql_server2022`); if that
|
||||||
|
host isn't reachable, the API won't start there. Development logs to console/file and boots cleanly
|
||||||
|
against the configured `SqlServer` DB. This is pre-existing infra config, unrelated to phase-0 code.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# Backend Phase 0 — Foundation, cross-cutting seams & starter cleanup — Report (2026-06-28)
|
||||||
|
|
||||||
|
## What was built
|
||||||
|
- **Starter cleanup.** Removed the `Order` demo end-to-end: entity (`Domain/Entities/Order`), feature
|
||||||
|
folder (`Application/Features/Order`), `IOrderRepository`/`OrderRepository`, `OrderConfig`,
|
||||||
|
`User.Orders` nav, `IUnitOfWork.OrderRepository`, the gRPC `OrderGrpcServices` + proto + wiring +
|
||||||
|
`/GrpcUserOrder` map. Deleted the three pre-marketplace migrations (`2021/2022/2023`) + snapshot.
|
||||||
|
- **Fresh baseline migration** `Migrations/20260628191947_InitialBaseline` — Identity (`usr` schema) +
|
||||||
|
`UserRefreshTokens`, with audit columns `CreatedAt`/`ModifiedAt` (`datetimeoffset`),
|
||||||
|
`CreatedById`/`ModifiedById` (`int`). No `Orders` table.
|
||||||
|
- **Audit base type.** `BaseEntity`/`IAuditableEntity` in `Domain/Common/BaseEntity.cs`:
|
||||||
|
`CreatedAt`/`ModifiedAt` as `DateTimeOffset`, `CreatedById`/`ModifiedById` as `int?`
|
||||||
|
(renamed from the old `CreatedTime`/`ModifiedDate` `DateTime` pair, to match `CONVENTIONS.md` §6).
|
||||||
|
- **Current-user plumbing.** `ICurrentUser` (`Application/Contracts/Common`) with
|
||||||
|
`HttpContextCurrentUser` + `NullCurrentUser` (`Infrastructure.Identity/Identity/CurrentUser/`),
|
||||||
|
registered Scoped; `AddHttpContextAccessor()` wired.
|
||||||
|
- **Audit interceptor.** `AuditFieldInterceptor` (`Infrastructure.Persistence/Interceptors/`), a
|
||||||
|
`SaveChangesInterceptor` stamping audit fields from `ICurrentUser` + `IDateTimeProvider`. Replaces
|
||||||
|
the old `DateTime.Now` date hook in `ApplicationDbContext` (the `_cleanString` Persian-normalisation
|
||||||
|
hook stays). Registered via `AddInterceptors` in `AddPersistenceServices`.
|
||||||
|
- **Five cross-cutting seams** (interfaces in `Application/Contracts/Common`, mocks in
|
||||||
|
`Infrastructure.CrossCutting/Seams/`, registered by `AddCrossCuttingSeams(config)`):
|
||||||
|
`IDateTimeProvider`→`SystemDateTimeProvider`, `IFieldEncryptor`→`SymmetricFieldEncryptor`,
|
||||||
|
`ICacheService`→`MemoryCacheService`, `IObjectStorage`→`LocalDiskObjectStorage`,
|
||||||
|
`INotificationDispatcher`→`LogNotificationDispatcher`.
|
||||||
|
- **Pipeline + rate limiting.** Registered `LoggingBehavior<,>` as the outermost Mediator behavior.
|
||||||
|
Added `AddRateLimitingPolicies()` (`WebFramework/ServiceConfiguration`) — built-in rate limiter with
|
||||||
|
a per-IP global limit + named policies `otp`/`auth`/`sensitive`; `app.UseRateLimiter()` placed
|
||||||
|
**before** `app.UseAuthentication()`.
|
||||||
|
- **REST surface.** `Controllers/V1/PingController` (sealed, `BaseController`, `ISender`) with
|
||||||
|
`GetStatus` and a rate-limited `GetStatusRateLimited`, backed by the `Features/System/Queries/Ping`
|
||||||
|
CQRS feature — proves REST → Mediator → behaviors → `OperationResult` → `ApiResult` envelope.
|
||||||
|
- **Tests.** New `Baya.Test.Foundation` project: `SymmetricFieldEncryptor` round-trip + deterministic
|
||||||
|
hash, `AuditFieldInterceptor` add/update stamping (SQLite), `PingQueryHandler` happy path.
|
||||||
|
|
||||||
|
## What is now testable (and exactly how)
|
||||||
|
- **Build/test gate:** `dotnet build Baya.sln` → 0 errors, 0 new warnings; `dotnet test Baya.sln` →
|
||||||
|
10 pass (4 existing identity + 6 new foundation). ✅ verified.
|
||||||
|
- **Live API:** ✅ verified end-to-end against SQL Server `192.168.100.14` (Development env). On boot the
|
||||||
|
`InitialBaseline` migration applied and the default users seeded. `dotnet run --project
|
||||||
|
src/API/Baya.Web.Api/...` → open `/swagger`:
|
||||||
|
- `GET /api/v1/ping/get_status` → `200` with body
|
||||||
|
`{ "data": { "service": "Baya.Web.Api", "status": "ok", "serverTimeUtc": "<utc>" },
|
||||||
|
"isSuccess": true, "statusCode": 200, "message": "Success", "requestId": "<trace>" }`
|
||||||
|
(the standard `ApiResult<T>` envelope). ✅
|
||||||
|
- `GET /api/v1/ping/get_status_rate_limited` from one IP → first 5 = `200`, then `429`. ✅
|
||||||
|
- The Order REST/gRPC endpoints are gone; the swagger doc shows only the two ping paths. ✅
|
||||||
|
|
||||||
|
> Run note: in non-Development the Serilog `MSSqlServer` sink targets the `logDb` connection
|
||||||
|
> (`Server=sql_server2022`), which must resolve or the host fails to build (pre-existing config,
|
||||||
|
> unrelated to this phase). Development logs to console/file, so verify there.
|
||||||
|
|
||||||
|
## What is mocked / waiting on a real service
|
||||||
|
All five seams are 🟡 (mock behind DI seam) — see `reports/mocks-registry.md` rows for
|
||||||
|
`IFieldEncryptor`, `ICacheService`, `IObjectStorage`, `INotificationDispatcher` (and `IDateTimeProvider`,
|
||||||
|
not external). Each mock lives in `Baya.Infrastructure.CrossCutting/Seams/`; swapping to a real provider
|
||||||
|
is a registration change in `AddCrossCuttingSeams`. `INotificationDispatcher` does **not** write yet —
|
||||||
|
the in-app `notifications` write lands in b15.
|
||||||
|
|
||||||
|
## Contracts
|
||||||
|
- **Produced:** the `ApiResult`/`OperationResult` envelope shape. The first machine `swagger.json`
|
||||||
|
snapshot is published at `dev/contracts/openapi/swagger.v1.json` (paths `ping/get_status` +
|
||||||
|
`ping/get_status_rate_limited`; schemas `ApiResult`, `ApiResultStatusCode`,
|
||||||
|
`ApiResultOfPingQueryResult`, `PingQueryResult`). Casing on the wire: **camelCase** body/envelope
|
||||||
|
properties, **snake_case** URL segments.
|
||||||
|
- **Consumed:** none.
|
||||||
|
|
||||||
|
## Docs updated
|
||||||
|
- `server/CLAUDE.md` — *Project map* (Order removed; CrossCutting `Seams/`, Persistence `Interceptors/`,
|
||||||
|
Identity `CurrentUser/`, `Baya.Test.Foundation`, new seams note), *Startup wiring* (new registrations
|
||||||
|
+ rate-limiter-before-auth pipeline order), CQRS example (Order → generic + Ping).
|
||||||
|
- `server/CONVENTIONS.md` — §6 "as built" note (audit base type + interceptor) and a new
|
||||||
|
"Money is IRR `BIGINT` — integer-only, no floats" rule.
|
||||||
|
- `dev/shared-working-context/reports/mocks-registry.md` — five seam rows → 🟡 with files + config keys.
|
||||||
|
|
||||||
|
## Follow-ups for later phases
|
||||||
|
- **b1:** extend `AuditFieldInterceptor` to also write append-only `audit_logs` rows; evolve the
|
||||||
|
baseline migration with the marketplace schema; `IHolidayCalendar` seed.
|
||||||
|
- **b2:** `ISmsSender` seam + auth/OTP REST surface; apply the `otp`/`auth` rate-limit policies to those
|
||||||
|
endpoints.
|
||||||
|
- **Integration tests:** a `WebApplicationFactory<Program>` test project (CONVENTIONS §10) was not
|
||||||
|
scaffolded this phase; add it when the first real feature area lands so the HTTP pipeline (routing,
|
||||||
|
auth, envelope translation, 429) is covered automatically.
|
||||||
|
- **Logging config (pre-existing):** the non-Development Serilog `MSSqlServer` sink points at
|
||||||
|
`logDb` = `Server=sql_server2022`. If that host isn't reachable in an environment, the API won't
|
||||||
|
start there. Out of scope for this phase — flag for whoever owns environment/infra config.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
All Definition-of-Done items met: build clean (0 new warnings), 10 tests green, REST controller live
|
||||||
|
through Swagger with the `OperationResult` envelope, `LoggingBehavior` + rate limiter wired,
|
||||||
|
`ICurrentUser` + audit interceptor + the five seams in place, migration applied + DB seeded, docs/
|
||||||
|
contracts/handoff/registry updated, swagger snapshot published. Live verification done against
|
||||||
|
`192.168.100.14`.
|
||||||
@@ -10,8 +10,8 @@ Status legend: 🔴 not built · 🟡 mocked (seam + fake impl in place) · 🟢
|
|||||||
| Seam (interface) | Introduced in | What it fakes | Config keys | Make it real → | Status |
|
| Seam (interface) | Introduced in | What it fakes | Config keys | Make it real → | Status |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| `ISmsSender` | backend-phase-2 | OTP/SMS delivery — logs the code instead of sending | _tbd_ | Implement a Kavenegar/Ghasedak/SMS.ir client; keep idempotency + rate-limit | 🔴 |
|
| `ISmsSender` | backend-phase-2 | OTP/SMS delivery — logs the code instead of sending | _tbd_ | Implement a Kavenegar/Ghasedak/SMS.ir client; keep idempotency + rate-limit | 🔴 |
|
||||||
| `IObjectStorage` | backend-phase-0/6 | File storage — local/in-memory instead of object store | _tbd_ | Point at MinIO/S3/ArvanCloud; presigned upload/download; bucket + creds | 🔴 |
|
| `IObjectStorage` | backend-phase-0/6 | File storage — local-disk store under a scratch root (`LocalDiskObjectStorage`, `Baya.Infrastructure.CrossCutting/Seams/`) | `Seams:ObjectStorage:RootPath` (default: temp dir) | Point at MinIO/S3/ArvanCloud; presigned upload/download; bucket + creds | 🟡 |
|
||||||
| `ICacheService` | backend-phase-0 | Caching — in-memory dictionary | _tbd_ | Swap to Redis (`StackExchange.Redis`); keep key/TTL scheme | 🔴 |
|
| `ICacheService` | backend-phase-0 | Caching — in-memory `IMemoryCache` (`MemoryCacheService`, `Baya.Infrastructure.CrossCutting/Seams/`) | _none_ | Swap to Redis (`StackExchange.Redis`); keep key/TTL scheme | 🟡 |
|
||||||
| `IDistributedLock` | backend-phase-10 | Money-path locks — no-op/in-proc | _tbd_ | Redis lock (RedLock); DB constraint remains the backstop | 🔴 |
|
| `IDistributedLock` | backend-phase-10 | Money-path locks — no-op/in-proc | _tbd_ | Redis lock (RedLock); DB constraint remains the backstop | 🔴 |
|
||||||
| `INurseSearch` | backend-phase-7 | Search — SQL over `nurse_search_index` | _tbd_ | Elasticsearch index + feeder; reimplement the interface | 🔴 |
|
| `INurseSearch` | backend-phase-7 | Search — SQL over `nurse_search_index` | _tbd_ | Elasticsearch index + feeder; reimplement the interface | 🔴 |
|
||||||
| `IPaymentProvider` | backend-phase-10 | Card PSP/IPG — deterministic success | _tbd_ | ZarinPal/Sadad/Vandar/Jibit + Shaparak; merchant/terminal/تسهیم | 🔴 |
|
| `IPaymentProvider` | backend-phase-10 | Card PSP/IPG — deterministic success | _tbd_ | ZarinPal/Sadad/Vandar/Jibit + Shaparak; merchant/terminal/تسهیم | 🔴 |
|
||||||
@@ -28,8 +28,8 @@ Status legend: 🔴 not built · 🟡 mocked (seam + fake impl in place) · 🟢
|
|||||||
| `IGeocoder` | backend-phase-4 | Address→lat/lng — echo/static | _tbd_ | Neshan/Google geocoding | 🔴 |
|
| `IGeocoder` | backend-phase-4 | Address→lat/lng — echo/static | _tbd_ | Neshan/Google geocoding | 🔴 |
|
||||||
| `IMoadianClient` | backend-phase-11 | سامانه مودیان e-invoice — leaves ref pending | _tbd_ | Real مودیان submission → 22-digit ref | 🔴 |
|
| `IMoadianClient` | backend-phase-11 | سامانه مودیان e-invoice — leaves ref pending | _tbd_ | Real مودیان submission → 22-digit ref | 🔴 |
|
||||||
| `IReviewModerationService` | backend-phase-14 | AI moderation — keyword/pass-through | _tbd_ | Real classifier/LLM endpoint | 🔴 |
|
| `IReviewModerationService` | backend-phase-14 | AI moderation — keyword/pass-through | _tbd_ | Real classifier/LLM endpoint | 🔴 |
|
||||||
| `IFieldEncryptor` | backend-phase-0 | PII encryption — local symmetric key | _tbd_ | KMS / column encryption / Key Vault / HSM | 🔴 |
|
| `IFieldEncryptor` | backend-phase-0 | PII encryption — AES-256-CBC + HMAC hash from a local symmetric key (`SymmetricFieldEncryptor`, `Baya.Infrastructure.CrossCutting/Seams/`) | `Seams:FieldEncryption:Key`, `Seams:FieldEncryption:HashKey` | KMS / column encryption / Key Vault / HSM | 🟡 |
|
||||||
| `INotificationDispatcher` | backend-phase-0/15 | Notification channels — in-app write only | _tbd_ | Add SMS/push (FCM); polling → Redis pub/sub or SignalR later | 🔴 |
|
| `INotificationDispatcher` | backend-phase-0/15 | Notification channels — logs/no-op (`LogNotificationDispatcher`, `Baya.Infrastructure.CrossCutting/Seams/`); no write yet | _none_ | Add the in-app `notifications` write (b15) + SMS/push (FCM); polling → Redis pub/sub or SignalR later | 🟡 |
|
||||||
| `ILicenseVerificationService` | backend-phase-15 | eNamad / MoH establishment-permit — manual approve | _tbd_ | Real registry/API | 🔴 |
|
| `ILicenseVerificationService` | backend-phase-15 | eNamad / MoH establishment-permit — manual approve | _tbd_ | Real registry/API | 🔴 |
|
||||||
|
|
||||||
> Exact config keys and file paths get filled in by the phase that builds each seam. Keep the
|
> Exact config keys and file paths get filled in by the phase that builds each seam. Keep the
|
||||||
|
|||||||
+115
@@ -52,60 +52,174 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||||||
docker-compose.yml = docker-compose.yml
|
docker-compose.yml = docker-compose.yml
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Baya.Test.Foundation", "src\Tests\Baya.Test.Foundation\Baya.Test.Foundation.csproj", "{052BF207-440C-4FAB-AF6F-4992B29A3BF4}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Release|Any CPU.Build.0 = Release|Any CPU
|
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{09E81356-0531-42A0-9F7F-00C495F1226E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{09E81356-0531-42A0-9F7F-00C495F1226E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{09E81356-0531-42A0-9F7F-00C495F1226E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{09E81356-0531-42A0-9F7F-00C495F1226E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{09E81356-0531-42A0-9F7F-00C495F1226E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{09E81356-0531-42A0-9F7F-00C495F1226E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{09E81356-0531-42A0-9F7F-00C495F1226E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{09E81356-0531-42A0-9F7F-00C495F1226E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{09E81356-0531-42A0-9F7F-00C495F1226E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{09E81356-0531-42A0-9F7F-00C495F1226E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{09E81356-0531-42A0-9F7F-00C495F1226E}.Release|Any CPU.Build.0 = Release|Any CPU
|
{09E81356-0531-42A0-9F7F-00C495F1226E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{09E81356-0531-42A0-9F7F-00C495F1226E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{09E81356-0531-42A0-9F7F-00C495F1226E}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{09E81356-0531-42A0-9F7F-00C495F1226E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{09E81356-0531-42A0-9F7F-00C495F1226E}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Release|Any CPU.Build.0 = Release|Any CPU
|
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Release|Any CPU.Build.0 = Release|Any CPU
|
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Release|Any CPU.Build.0 = Release|Any CPU
|
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Release|Any CPU.Build.0 = Release|Any CPU
|
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Release|Any CPU.Build.0 = Release|Any CPU
|
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Release|Any CPU.Build.0 = Release|Any CPU
|
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Release|Any CPU.Build.0 = Release|Any CPU
|
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Release|Any CPU.Build.0 = Release|Any CPU
|
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Release|Any CPU.Build.0 = Release|Any CPU
|
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Release|Any CPU.Build.0 = Release|Any CPU
|
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{052BF207-440C-4FAB-AF6F-4992B29A3BF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{052BF207-440C-4FAB-AF6F-4992B29A3BF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{052BF207-440C-4FAB-AF6F-4992B29A3BF4}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{052BF207-440C-4FAB-AF6F-4992B29A3BF4}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{052BF207-440C-4FAB-AF6F-4992B29A3BF4}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{052BF207-440C-4FAB-AF6F-4992B29A3BF4}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{052BF207-440C-4FAB-AF6F-4992B29A3BF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{052BF207-440C-4FAB-AF6F-4992B29A3BF4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{052BF207-440C-4FAB-AF6F-4992B29A3BF4}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{052BF207-440C-4FAB-AF6F-4992B29A3BF4}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{052BF207-440C-4FAB-AF6F-4992B29A3BF4}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{052BF207-440C-4FAB-AF6F-4992B29A3BF4}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -132,6 +246,7 @@ Global
|
|||||||
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60} = {45FA88C0-9986-40E5-A2E2-7742302518D2}
|
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60} = {45FA88C0-9986-40E5-A2E2-7742302518D2}
|
||||||
{7699705C-2C15-467F-957D-4C5EBE4FD92E} = {2373AFFC-1389-4D78-8465-074AB22084AF}
|
{7699705C-2C15-467F-957D-4C5EBE4FD92E} = {2373AFFC-1389-4D78-8465-074AB22084AF}
|
||||||
{704FAE1E-F0D2-468E-8B3D-E9E6F323ABE8} = {42CAB060-5D50-4E18-8F85-EBA5EB85B268}
|
{704FAE1E-F0D2-468E-8B3D-E9E6F323ABE8} = {42CAB060-5D50-4E18-8F85-EBA5EB85B268}
|
||||||
|
{052BF207-440C-4FAB-AF6F-4992B29A3BF4} = {77986571-8153-4120-AD08-36729310A56B}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {05C223B9-EA89-44B2-B9F5-D01181F85DFE}
|
SolutionGuid = {05C223B9-EA89-44B2-B9F5-D01181F85DFE}
|
||||||
|
|||||||
+39
-23
@@ -81,27 +81,36 @@ projects/assemblies, Clean-Architecture layers, and cross-layer dependencies.
|
|||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── Core/
|
├── Core/
|
||||||
│ ├── Baya.Domain Entities (User, Order, Role…), BaseEntity, IEntity, ITimeModification
|
│ ├── Baya.Domain Entities (User, Role…), BaseEntity, IEntity, ITimeModification, IAuditableEntity
|
||||||
│ └── Baya.Application Features/ (Commands & Queries), Contracts/, Models/, pipeline behaviors (Common/)
|
│ └── Baya.Application Features/ (Commands & Queries), Contracts/ (incl. Contracts/Common cross-cutting seams), Models/, pipeline behaviors (Common/)
|
||||||
├── Infrastructure/
|
├── Infrastructure/
|
||||||
│ ├── Baya.Infrastructure.Persistence ApplicationDbContext, Repositories/, Configuration/, Migrations/
|
│ ├── Baya.Infrastructure.Persistence ApplicationDbContext, Repositories/, Configuration/, Migrations/, Interceptors/ (AuditFieldInterceptor)
|
||||||
│ ├── Baya.Infrastructure.Identity Jwt/, Identity/ (Managers, Stores, PermissionManager, Seed)
|
│ ├── Baya.Infrastructure.Identity Jwt/, Identity/ (Managers, Stores, PermissionManager, Seed, CurrentUser/)
|
||||||
│ ├── Baya.Infrastructure.CrossCutting Serilog wiring
|
│ ├── Baya.Infrastructure.CrossCutting Serilog wiring + Seams/ (mock impls of the cross-cutting seams) + AddCrossCuttingSeams
|
||||||
│ └── Baya.Infrastructure.Monitoring HealthChecks, OpenTelemetry, prometheus-net
|
│ └── Baya.Infrastructure.Monitoring HealthChecks, OpenTelemetry, prometheus-net
|
||||||
├── API/
|
├── API/
|
||||||
│ ├── Baya.Web.Api Program.cs, Controllers/V1/, appsettings*.json
|
│ ├── Baya.Web.Api Program.cs, Controllers/V1/ (PingController), appsettings*.json
|
||||||
│ ├── Baya.WebFramework BaseController, Filters/, Middlewares/, Swagger/, Routing/
|
│ ├── Baya.WebFramework BaseController, Filters/, Middlewares/, Swagger/, Routing/, ServiceConfiguration/ (rate limiting)
|
||||||
│ └── Plugins/Baya.Web.Plugins.Grpc gRPC services + .proto models
|
│ └── Plugins/Baya.Web.Plugins.Grpc gRPC services + .proto models (User only)
|
||||||
├── Shared/Baya.SharedKernel Extensions + validation base
|
├── Shared/Baya.SharedKernel Extensions + validation base
|
||||||
└── Tests/
|
└── Tests/
|
||||||
├── Baya.Tests.Setup Shared test infrastructure (SQLite, NSubstitute setup)
|
├── Baya.Tests.Setup Shared test infrastructure (SQLite, NSubstitute setup)
|
||||||
└── Baya.Test.Infrastructure.Identity xUnit identity tests
|
├── Baya.Test.Infrastructure.Identity xUnit identity tests
|
||||||
|
└── Baya.Test.Foundation xUnit tests for cross-cutting plumbing (encryptor, audit interceptor, ping)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Dependency direction points inward.** Domain has no dependencies. Application depends only on
|
**Dependency direction points inward.** Domain has no dependencies. Application depends only on
|
||||||
Domain. Infrastructure and API implement/consume Application contracts. Never make Domain or
|
Domain. Infrastructure and API implement/consume Application contracts. Never make Domain or
|
||||||
Application reference Infrastructure or the API — this is a hard rule.
|
Application reference Infrastructure or the API — this is a hard rule.
|
||||||
|
|
||||||
|
**Cross-cutting seams.** Application defines mock-able external dependencies as interfaces in
|
||||||
|
`Contracts/Common/` (`IDateTimeProvider`, `IFieldEncryptor`, `ICacheService`, `IObjectStorage`,
|
||||||
|
`INotificationDispatcher`, plus `ICurrentUser`). Their in-memory/local mock implementations live in
|
||||||
|
`Baya.Infrastructure.CrossCutting/Seams/` and are registered by `AddCrossCuttingSeams(configuration)`
|
||||||
|
(config section `Seams`); `ICurrentUser` is registered in the Identity layer. Swapping a mock for a
|
||||||
|
real provider is a registration change — handlers depend only on the contract. Audit fields are
|
||||||
|
stamped by `AuditFieldInterceptor` (Persistence), not in handlers.
|
||||||
|
|
||||||
**Keeping the Project map current.** When a change touches the architecture — adds, removes, or
|
**Keeping the Project map current.** When a change touches the architecture — adds, removes, or
|
||||||
renames a project/assembly, a Clean-Architecture layer, or a major folder, or changes a cross-layer
|
renames a project/assembly, a Clean-Architecture layer, or a major folder, or changes a cross-layer
|
||||||
dependency — you **must** update this Project map (and the dependency rule above, if affected) in the
|
dependency — you **must** update this Project map (and the dependency rule above, if affected) in the
|
||||||
@@ -116,16 +125,20 @@ Service registration is composed from per-layer extension methods (each project'
|
|||||||
|
|
||||||
```
|
```
|
||||||
ConfigureHealthChecks() · SetupOpenTelemetry()
|
ConfigureHealthChecks() · SetupOpenTelemetry()
|
||||||
AddApplicationServices() // Mediator + validators + pipeline behaviors
|
AddApplicationServices() // Mediator + pipeline behaviors (Logging → Metrics → Validate)
|
||||||
RegisterIdentityServices(...) // Identity, JWT/JWE, authorization policies
|
RegisterIdentityServices(...) // Identity, JWT/JWE, authorization policies, ICurrentUser + IHttpContextAccessor
|
||||||
AddPersistenceServices(...) // DbContext, UnitOfWork, repositories
|
AddPersistenceServices(...) // DbContext (+ AuditFieldInterceptor), UnitOfWork, repositories
|
||||||
|
AddCrossCuttingSeams(config) // IDateTimeProvider, IFieldEncryptor, ICacheService, IObjectStorage, INotificationDispatcher (mocks)
|
||||||
AddWebFrameworkServices() // API versioning + snake_case routing
|
AddWebFrameworkServices() // API versioning + snake_case routing
|
||||||
|
AddRateLimitingPolicies() // built-in rate limiter: per-IP global + named (otp/auth/sensitive)
|
||||||
AddSwagger("v1", "v1.1") · RegisterValidatorsAsServices() · AddMapster()
|
AddSwagger("v1", "v1.1") · RegisterValidatorsAsServices() · AddMapster()
|
||||||
ConfigureGrpcPluginServices()
|
ConfigureGrpcPluginServices()
|
||||||
```
|
```
|
||||||
|
|
||||||
Pipeline order: exception handler → Swagger → routing → **authentication → authorization** →
|
Pipeline order: exception handler → Swagger → routing → **rate limiter → authentication →
|
||||||
controllers → metrics → health checks → gRPC.
|
authorization** → controllers → metrics → health checks → gRPC. `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` —
|
When adding new infrastructure, expose it as an extension method and call it from `Program.cs` —
|
||||||
never inline registrations there directly.
|
never inline registrations there directly.
|
||||||
@@ -137,17 +150,20 @@ never inline registrations there directly.
|
|||||||
Features live under `Baya.Application/Features/<Area>/{Commands|Queries}/<Name>/`:
|
Features live under `Baya.Application/Features/<Area>/{Commands|Queries}/<Name>/`:
|
||||||
|
|
||||||
```
|
```
|
||||||
Features/Order/
|
Features/<Area>/
|
||||||
├── Commands/CreateOrderCommand/
|
├── Commands/<VerbNoun>Command/
|
||||||
│ ├── CreateOrderCommand.cs record : ICommand<OperationResult<T>>
|
│ ├── <VerbNoun>Command.cs record : IRequest<OperationResult<T>>
|
||||||
│ ├── CreateOrderCommand.Handler.cs internal sealed class : ICommandHandler<...>
|
│ ├── <VerbNoun>Command.Handler.cs internal sealed class : IRequestHandler<...>
|
||||||
│ └── CreateOrderCommand.Validator.cs
|
│ └── <VerbNoun>Command.Validator.cs
|
||||||
└── Queries/GetUserOrdersQuery/
|
└── Queries/<VerbNoun>Query/
|
||||||
├── GetUserOrdersQuery.cs
|
├── <VerbNoun>Query.cs
|
||||||
├── GetUserOrdersQuery.Handler.cs
|
├── <VerbNoun>Query.Handler.cs
|
||||||
└── GetUserOrdersQuery.Result.cs
|
└── <VerbNoun>Query.Result.cs
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A minimal live example shipped in backend-phase-0: `Features/System/Queries/Ping/` (query + handler +
|
||||||
|
result), surfaced by `Controllers/V1/PingController`.
|
||||||
|
|
||||||
Handlers are `internal sealed`. Requests are `record` types. Validators use FluentValidation and are
|
Handlers are `internal sealed`. Requests are `record` types. Validators use FluentValidation and are
|
||||||
picked up automatically by the `ValidateCommandBehavior` pipeline behavior. Never throw for expected
|
picked up automatically by the `ValidateCommandBehavior` pipeline behavior. Never throw for expected
|
||||||
failures — use `OperationResult` factory methods.
|
failures — use `OperationResult` factory methods.
|
||||||
|
|||||||
@@ -270,6 +270,16 @@ When designing or extending an entity, include audit fields alongside timestamps
|
|||||||
|
|
||||||
Wire `ICurrentUser` (HTTP context accessor wrapped in an interface, registered Scoped) into `ApplicationDbContext` so the context can stamp who made the change without handlers needing to pass it explicitly. Audit fields cannot be backfilled retroactively — design them in from the start.
|
Wire `ICurrentUser` (HTTP context accessor wrapped in an interface, registered Scoped) into `ApplicationDbContext` so the context can stamp who made the change without handlers needing to pass it explicitly. Audit fields cannot be backfilled retroactively — design them in from the start.
|
||||||
|
|
||||||
|
> **As built (backend-phase-0):** the audit base type is `BaseEntity`/`IAuditableEntity` in
|
||||||
|
> `Baya.Domain/Common/BaseEntity.cs` (`CreatedAt`/`ModifiedAt` as `DateTimeOffset`, `CreatedById`/
|
||||||
|
> `ModifiedById` as `int?`). Stamping is done by `AuditFieldInterceptor`
|
||||||
|
> (`Baya.Infrastructure.Persistence/Interceptors/`), a `SaveChangesInterceptor` that reads time from
|
||||||
|
> `IDateTimeProvider` and the user from `ICurrentUser` — not in the `DbContext` itself.
|
||||||
|
|
||||||
|
### Money is IRR `BIGINT` — integer-only, no floats
|
||||||
|
|
||||||
|
Every monetary value is **IRR Rials stored as `long` / `BIGINT`**. There is **no float/decimal path** on money — not in entities, DTOs, the API, or arithmetic. Toman is display-only and converts to/from Rials **only** inside a provider adapter at its boundary, never in domain or shared code. If a money value object is introduced later it must be integer-only. The three booking amounts always satisfy `gross = commission + payout`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Validation
|
## 7. Validation
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Asp.Versioning;
|
||||||
|
using Baya.Application.Features.System.Queries.Ping;
|
||||||
|
using Baya.WebFramework.Attributes;
|
||||||
|
using Baya.WebFramework.BaseController;
|
||||||
|
using Mediator;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
using Baya.WebFramework.ServiceConfiguration;
|
||||||
|
|
||||||
|
namespace Baya.Web.Api.Controllers.V1;
|
||||||
|
|
||||||
|
[ApiVersion("1")]
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/v{version:apiVersion}/[controller]")]
|
||||||
|
[Display(Description = "Service liveness and pipeline-smoke endpoints")]
|
||||||
|
public sealed class PingController(ISender sender) : BaseController
|
||||||
|
{
|
||||||
|
[HttpGet("[action]")]
|
||||||
|
[ProducesOkApiResponseType<PingQueryResult>]
|
||||||
|
public async Task<IActionResult> GetStatus(CancellationToken cancellationToken)
|
||||||
|
=> OperationResult(await sender.Send(new PingQuery(), cancellationToken));
|
||||||
|
|
||||||
|
[HttpGet("[action]")]
|
||||||
|
[EnableRateLimiting(RateLimitingServiceExtension.GlobalPolicy)]
|
||||||
|
[ProducesOkApiResponseType<PingQueryResult>]
|
||||||
|
public async Task<IActionResult> GetStatusRateLimited(CancellationToken cancellationToken)
|
||||||
|
=> OperationResult(await sender.Send(new PingQuery(), cancellationToken));
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ using Baya.Application.Models.Identity;
|
|||||||
using Baya.Application.ServiceConfiguration;
|
using Baya.Application.ServiceConfiguration;
|
||||||
using Baya.Domain.Entities.User;
|
using Baya.Domain.Entities.User;
|
||||||
using Baya.Infrastructure.CrossCutting.Logging;
|
using Baya.Infrastructure.CrossCutting.Logging;
|
||||||
|
using Baya.Infrastructure.CrossCutting.ServiceConfiguration;
|
||||||
using Baya.Infrastructure.Identity.Identity.Dtos;
|
using Baya.Infrastructure.Identity.Identity.Dtos;
|
||||||
using Baya.Infrastructure.Identity.Jwt;
|
using Baya.Infrastructure.Identity.Jwt;
|
||||||
using Baya.Infrastructure.Identity.ServiceConfiguration;
|
using Baya.Infrastructure.Identity.ServiceConfiguration;
|
||||||
@@ -69,7 +70,9 @@ builder.Services.AddSwagger("v1","v1.1");
|
|||||||
builder.Services.AddApplicationServices()
|
builder.Services.AddApplicationServices()
|
||||||
.RegisterIdentityServices(identitySettings)
|
.RegisterIdentityServices(identitySettings)
|
||||||
.AddPersistenceServices(configuration)
|
.AddPersistenceServices(configuration)
|
||||||
.AddWebFrameworkServices();
|
.AddCrossCuttingSeams(configuration)
|
||||||
|
.AddWebFrameworkServices()
|
||||||
|
.AddRateLimitingPolicies();
|
||||||
|
|
||||||
builder.Services.RegisterValidatorsAsServices();
|
builder.Services.RegisterValidatorsAsServices();
|
||||||
builder.Services.AddExceptionHandler<ExceptionHandler>();
|
builder.Services.AddExceptionHandler<ExceptionHandler>();
|
||||||
@@ -105,6 +108,8 @@ app.UseSwaggerAndUi();
|
|||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|
||||||
|
app.UseRateLimiter();
|
||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"SqlServer": "Server=sql_server2022;Database=Baya_DB_Docker;User Id=SA;Password=A&VeryComplex123Password;MultipleActiveResultSets=true;encrypt=false",
|
"SqlServer": "Server=192.168.100.14,1433;Database=Baya;User Id=sa;Password=Pa##w0rd;TrustServerCertificate=True;Encrypt=False;",
|
||||||
"logDb": "Server=sql_server2022;Database=Baya_Log_DB_Docker;User Id=SA;Password=A&VeryComplex123Password;MultipleActiveResultSets=true;encrypt=false"
|
"logDb": "Server=sql_server2022;Database=Baya_Log_DB_Docker;User Id=SA;Password=A&VeryComplex123Password;MultipleActiveResultSets=true;encrypt=false"
|
||||||
},
|
},
|
||||||
"IdentitySettings": {
|
"IdentitySettings": {
|
||||||
@@ -11,6 +11,15 @@
|
|||||||
"NotBeforeMinutes": "0",
|
"NotBeforeMinutes": "0",
|
||||||
"ExpirationMinutes": "10000"
|
"ExpirationMinutes": "10000"
|
||||||
},
|
},
|
||||||
|
"Seams": {
|
||||||
|
"FieldEncryption": {
|
||||||
|
"Key": "local-dev-field-encryption-key-change-me",
|
||||||
|
"HashKey": "local-dev-field-hash-key-change-me"
|
||||||
|
},
|
||||||
|
"ObjectStorage": {
|
||||||
|
"RootPath": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"Kestrel": {
|
"Kestrel": {
|
||||||
"EndpointDefaults": {
|
"EndpointDefaults": {
|
||||||
|
|||||||
@@ -20,4 +20,8 @@
|
|||||||
<ProjectReference Include="..\..\Infrastructure\Baya.Infrastructure.Persistence\Baya.Infrastructure.Persistence.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Baya.Infrastructure.Persistence\Baya.Infrastructure.Persistence.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using System.Threading.RateLimiting;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Baya.WebFramework.ServiceConfiguration;
|
||||||
|
|
||||||
|
public static class RateLimitingServiceExtension
|
||||||
|
{
|
||||||
|
/// <summary>Per-IP baseline applied to every endpoint that doesn't opt into a named policy.</summary>
|
||||||
|
public const string GlobalPolicy = "global";
|
||||||
|
|
||||||
|
/// <summary>Tighter limit for OTP request/verify endpoints (applied in backend-phase-2).</summary>
|
||||||
|
public const string OtpPolicy = "otp";
|
||||||
|
|
||||||
|
/// <summary>Limit for login/refresh and other auth endpoints.</summary>
|
||||||
|
public const string AuthPolicy = "auth";
|
||||||
|
|
||||||
|
/// <summary>Limit for money-sensitive actions (refund/payout) applied in later phases.</summary>
|
||||||
|
public const string SensitivePolicy = "sensitive";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers the built-in rate limiter with a per-IP global limit plus named policies that
|
||||||
|
/// auth/OTP/sensitive endpoints opt into via <c>[EnableRateLimiting(name)]</c>. Over-limit
|
||||||
|
/// requests get <c>429 Too Many Requests</c>. Pair with <c>app.UseRateLimiter()</c> placed
|
||||||
|
/// before <c>app.UseAuthentication()</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddRateLimitingPolicies(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddRateLimiter(options =>
|
||||||
|
{
|
||||||
|
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||||
|
|
||||||
|
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
|
||||||
|
RateLimitPartition.GetFixedWindowLimiter(
|
||||||
|
PartitionKey(context),
|
||||||
|
_ => new FixedWindowRateLimiterOptions
|
||||||
|
{
|
||||||
|
PermitLimit = 100,
|
||||||
|
Window = TimeSpan.FromMinutes(1),
|
||||||
|
QueueLimit = 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddFixedWindowPolicy(options, OtpPolicy, permitLimit: 5, windowSeconds: 60);
|
||||||
|
AddFixedWindowPolicy(options, AuthPolicy, permitLimit: 10, windowSeconds: 60);
|
||||||
|
AddFixedWindowPolicy(options, SensitivePolicy, permitLimit: 20, windowSeconds: 60);
|
||||||
|
|
||||||
|
// A deliberately tiny policy used by the phase-0 ping endpoint to demonstrate 429s.
|
||||||
|
AddFixedWindowPolicy(options, GlobalPolicy, permitLimit: 5, windowSeconds: 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddFixedWindowPolicy(RateLimiterOptions options, string name, int permitLimit, int windowSeconds)
|
||||||
|
{
|
||||||
|
options.AddPolicy(name, context =>
|
||||||
|
RateLimitPartition.GetFixedWindowLimiter(
|
||||||
|
PartitionKey(context),
|
||||||
|
_ => new FixedWindowRateLimiterOptions
|
||||||
|
{
|
||||||
|
PermitLimit = permitLimit,
|
||||||
|
Window = TimeSpan.FromSeconds(windowSeconds),
|
||||||
|
QueueLimit = 0
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string PartitionKey(HttpContext context) =>
|
||||||
|
context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||||||
|
}
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Protobuf Include="ProtoModels\UserGrpcServiceModels.proto" GrpcServices="Server" />
|
<Protobuf Include="ProtoModels\UserGrpcServiceModels.proto" GrpcServices="Server" />
|
||||||
<Protobuf Include="ProtoModels\OrderGrpcServiceModels.proto" GrpcServices="Server" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ public static class GrpcPluginStartup
|
|||||||
{
|
{
|
||||||
|
|
||||||
app.MapGrpcService<UserGrpcServices>();
|
app.MapGrpcService<UserGrpcServices>();
|
||||||
app.MapGrpcService<OrderGrpcServices>();
|
|
||||||
app.MapGrpcReflectionService();
|
app.MapGrpcReflectionService();
|
||||||
|
|
||||||
app.MapGet("/GrpcUser", async context =>
|
app.MapGet("/GrpcUser", async context =>
|
||||||
@@ -29,11 +28,5 @@ public static class GrpcPluginStartup
|
|||||||
await context.Response.WriteAsync(
|
await context.Response.WriteAsync(
|
||||||
"Communication with this gRPC endpoint must be made through a gRPC client.");
|
"Communication with this gRPC endpoint must be made through a gRPC client.");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.MapGet("/GrpcUserOrder", async context =>
|
|
||||||
{
|
|
||||||
await context.Response.WriteAsync(
|
|
||||||
"Communication with this gRPC endpoint must be made through a gRPC client.");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
option csharp_namespace = "Baya.Web.Plugins.Grpc.ProtoModels";
|
|
||||||
import "google/protobuf/empty.proto";
|
|
||||||
|
|
||||||
package GrpcOrderController;
|
|
||||||
|
|
||||||
|
|
||||||
service OrderServices {
|
|
||||||
rpc GetUserOrders(google.protobuf.Empty) returns (stream GetUserOrdersModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
message GetUserOrdersModel{
|
|
||||||
int32 OrderId=1;
|
|
||||||
string OrderName=2;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
using Baya.Application.Features.Order.Queries.GetUserOrders;
|
|
||||||
using Baya.SharedKernel.Extensions;
|
|
||||||
using Baya.Web.Plugins.Grpc.ProtoModels;
|
|
||||||
using Google.Protobuf.WellKnownTypes;
|
|
||||||
using Grpc.Core;
|
|
||||||
using Mediator;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
|
|
||||||
namespace Baya.Web.Plugins.Grpc.Services
|
|
||||||
{
|
|
||||||
[Authorize]
|
|
||||||
public class OrderGrpcServices:OrderServices.OrderServicesBase
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
private readonly ISender _sender;
|
|
||||||
|
|
||||||
public OrderGrpcServices(ISender sender)
|
|
||||||
{
|
|
||||||
_sender = sender;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task GetUserOrders(Empty request, IServerStreamWriter<GetUserOrdersModel> responseStream, ServerCallContext context)
|
|
||||||
{
|
|
||||||
var userId = int.Parse(context.GetHttpContext().User.Identity.GetUserId());
|
|
||||||
|
|
||||||
var query = await _sender.Send(new GetUserOrdersQueryModel(userId));
|
|
||||||
|
|
||||||
if (!query.IsSuccess)
|
|
||||||
{
|
|
||||||
context.Status = new Status(StatusCode.InvalidArgument, query.GetErrorMessage());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var getUsersQueryResultModel in query.Result)
|
|
||||||
{
|
|
||||||
await responseStream.WriteAsync(new GetUserOrdersModel()
|
|
||||||
{ OrderId = getUsersQueryResultModel.OrderId, OrderName = getUsersQueryResultModel.OrderName });
|
|
||||||
|
|
||||||
await Task.Delay(400);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,9 @@
|
|||||||
<EmbeddedResource Remove="Common\ValidationBase\**" />
|
<EmbeddedResource Remove="Common\ValidationBase\**" />
|
||||||
<None Remove="Common\ValidationBase\**" />
|
<None Remove="Common\ValidationBase\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="Baya.Test.Foundation" />
|
||||||
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Baya.Domain\Baya.Domain.csproj" />
|
<ProjectReference Include="..\Baya.Domain\Baya.Domain.csproj" />
|
||||||
<PackageReference Include="Mediator.SourceGenerator">
|
<PackageReference Include="Mediator.SourceGenerator">
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#nullable enable
|
||||||
|
namespace Baya.Application.Contracts.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Typed caching seam for read-heavy and reference data. The mock is backed by in-process memory; the
|
||||||
|
/// real implementation swaps to Redis while keeping the same key/TTL scheme.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICacheService
|
||||||
|
{
|
||||||
|
ValueTask<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
ValueTask SetAsync<T>(string key, T value, TimeSpan? ttl = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
ValueTask RemoveAsync(string key, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
ValueTask<T> GetOrCreateAsync<T>(
|
||||||
|
string key,
|
||||||
|
Func<CancellationToken, ValueTask<T>> factory,
|
||||||
|
TimeSpan? ttl = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Baya.Application.Contracts.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ambient accessor for the authenticated caller, wrapping the HTTP context behind an Application
|
||||||
|
/// contract. Registered Scoped. A null-object implementation serves non-HTTP contexts (jobs, tests)
|
||||||
|
/// so the audit interceptor and handlers never depend on <c>IHttpContextAccessor</c> directly.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICurrentUser
|
||||||
|
{
|
||||||
|
int? UserId { get; }
|
||||||
|
|
||||||
|
bool IsAuthenticated { get; }
|
||||||
|
|
||||||
|
IReadOnlyList<string> Roles { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Baya.Application.Contracts.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abstracts the system clock so handlers and the audit interceptor never call <c>DateTime.Now</c>
|
||||||
|
/// directly and time can be frozen in tests.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDateTimeProvider
|
||||||
|
{
|
||||||
|
DateTimeOffset UtcNow { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
namespace Baya.Application.Contracts.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seam for field-level PII encryption (phone, national id, IBAN, addresses, clinical notes).
|
||||||
|
/// Every encrypted-at-rest column flows through this. The mock uses a local symmetric key; the real
|
||||||
|
/// implementation swaps to a KMS / column-encryption / Key Vault provider without touching callers.
|
||||||
|
/// Implementations must never log or surface plaintext.
|
||||||
|
/// </summary>
|
||||||
|
public interface IFieldEncryptor
|
||||||
|
{
|
||||||
|
string Encrypt(string plaintext);
|
||||||
|
|
||||||
|
string Decrypt(string ciphertext);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deterministic keyed hash for equality lookups on encrypted columns (e.g. <c>iban_hash</c>) —
|
||||||
|
/// the same input always yields the same hash so it can be indexed and queried.
|
||||||
|
/// </summary>
|
||||||
|
string Hash(string value);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace Baya.Application.Contracts.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The channel a notification is delivered through. Only <see cref="InApp"/> is wired now; SMS/push
|
||||||
|
/// arrive in later phases behind the same seam.
|
||||||
|
/// </summary>
|
||||||
|
public enum NotificationChannel
|
||||||
|
{
|
||||||
|
InApp,
|
||||||
|
Sms,
|
||||||
|
Push
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A notification to dispatch to a recipient over one channel.</summary>
|
||||||
|
/// <param name="RecipientUserId">The target user.</param>
|
||||||
|
/// <param name="Channel">Delivery channel.</param>
|
||||||
|
/// <param name="Title">Short title/subject.</param>
|
||||||
|
/// <param name="Body">Message body (no secrets/PII in logs).</param>
|
||||||
|
public sealed record Notification(
|
||||||
|
int RecipientUserId,
|
||||||
|
NotificationChannel Channel,
|
||||||
|
string Title,
|
||||||
|
string Body);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seam for emitting notifications from domains like booking and payments. The mock logs/no-ops; the
|
||||||
|
/// real in-app write lands in backend-phase-15, with SMS/push added behind the same interface.
|
||||||
|
/// </summary>
|
||||||
|
public interface INotificationDispatcher
|
||||||
|
{
|
||||||
|
ValueTask DispatchAsync(Notification notification, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#nullable enable
|
||||||
|
namespace Baya.Application.Contracts.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Blob/object storage seam keyed by an opaque storage key. The mock stores blobs on local disk; the
|
||||||
|
/// real implementation swaps to MinIO/S3/ArvanCloud (presigned URLs) without changing callers.
|
||||||
|
/// </summary>
|
||||||
|
public interface IObjectStorage
|
||||||
|
{
|
||||||
|
ValueTask PutAsync(
|
||||||
|
string key,
|
||||||
|
Stream content,
|
||||||
|
string contentType,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
ValueTask<Stream?> GetAsync(string key, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
ValueTask DeleteAsync(string key, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>A retrievable URL for the stored object (presigned by the real provider).</summary>
|
||||||
|
string GetUrl(string key);
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using Baya.Domain.Entities.Order;
|
|
||||||
|
|
||||||
namespace Baya.Application.Contracts.Persistence;
|
|
||||||
|
|
||||||
public interface IOrderRepository
|
|
||||||
{
|
|
||||||
Task AddOrderAsync(Order order);
|
|
||||||
Task<List<Order>> GetAllUserOrdersAsync(int userId);
|
|
||||||
Task<List<Order>> GetAllOrdersWithRelatedUserAsync();
|
|
||||||
Task<Order> GetUserOrderByIdAndUserIdAsync(int userId,int orderId,bool trackEntity);
|
|
||||||
Task DeleteUserOrdersAsync(int userId);
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
public interface IUnitOfWork
|
public interface IUnitOfWork
|
||||||
{
|
{
|
||||||
public IUserRefreshTokenRepository UserRefreshTokenRepository { get; }
|
public IUserRefreshTokenRepository UserRefreshTokenRepository { get; }
|
||||||
public IOrderRepository OrderRepository { get; }
|
|
||||||
Task CommitAsync();
|
Task CommitAsync();
|
||||||
ValueTask RollBackAsync();
|
ValueTask RollBackAsync();
|
||||||
}
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using Baya.Application.Contracts.Identity;
|
|
||||||
using Baya.Application.Contracts.Persistence;
|
|
||||||
using Baya.Application.Models.Common;
|
|
||||||
using Mediator;
|
|
||||||
|
|
||||||
namespace Baya.Application.Features.Order.Commands;
|
|
||||||
|
|
||||||
internal class AddOrderCommandHandler(IUnitOfWork unitOfWork, IAppUserManager userManager)
|
|
||||||
: IRequestHandler<AddOrderCommand, OperationResult<bool>>
|
|
||||||
{
|
|
||||||
public async ValueTask<OperationResult<bool>> Handle(AddOrderCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var user = await userManager.GetUserByIdAsync(request.UserId);
|
|
||||||
|
|
||||||
if(user==null)
|
|
||||||
return OperationResult<bool>.FailureResult("User Not Found");
|
|
||||||
|
|
||||||
await unitOfWork.OrderRepository.AddOrderAsync(new Domain.Entities.Order.Order()
|
|
||||||
{ UserId = user.Id, OrderName = request.OrderName });
|
|
||||||
|
|
||||||
await unitOfWork.CommitAsync();
|
|
||||||
|
|
||||||
return OperationResult<bool>.SuccessResult(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
using Baya.Application.Models.Common;
|
|
||||||
using Baya.SharedKernel.ValidationBase;
|
|
||||||
using Baya.SharedKernel.ValidationBase.Contracts;
|
|
||||||
using FluentValidation;
|
|
||||||
using Mediator;
|
|
||||||
|
|
||||||
namespace Baya.Application.Features.Order.Commands;
|
|
||||||
|
|
||||||
public record AddOrderCommand( string OrderName) : IRequest<OperationResult<bool>>,
|
|
||||||
IValidatableModel<AddOrderCommand>
|
|
||||||
{
|
|
||||||
[JsonIgnore]
|
|
||||||
public int UserId { get; set; }
|
|
||||||
|
|
||||||
public IValidator<AddOrderCommand> ValidateApplicationModel(ApplicationBaseValidationModelProvider<AddOrderCommand> validator)
|
|
||||||
{
|
|
||||||
validator.RuleFor(c => c.OrderName)
|
|
||||||
.NotEmpty()
|
|
||||||
.NotNull()
|
|
||||||
.WithMessage("Please enter your role name");
|
|
||||||
|
|
||||||
return validator;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
using Baya.Application.Contracts.Persistence;
|
|
||||||
using Baya.Application.Models.Common;
|
|
||||||
using Mediator;
|
|
||||||
|
|
||||||
namespace Baya.Application.Features.Order.Commands;
|
|
||||||
|
|
||||||
public class DeleteUserOrdersCommandHandler(IUnitOfWork unitOfWork) : IRequestHandler<DeleteUserOrdersCommand,OperationResult<bool>>
|
|
||||||
{
|
|
||||||
public async ValueTask<OperationResult<bool>> Handle(DeleteUserOrdersCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await unitOfWork.OrderRepository.DeleteUserOrdersAsync(request.UserId);
|
|
||||||
|
|
||||||
return OperationResult<bool>.SuccessResult(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
using Baya.Application.Models.Common;
|
|
||||||
using Mediator;
|
|
||||||
|
|
||||||
namespace Baya.Application.Features.Order.Commands;
|
|
||||||
|
|
||||||
public record DeleteUserOrdersCommand(int UserId):IRequest<OperationResult<bool>>;
|
|
||||||
-25
@@ -1,25 +0,0 @@
|
|||||||
using Baya.Application.Contracts.Persistence;
|
|
||||||
using Baya.Application.Models.Common;
|
|
||||||
using Mediator;
|
|
||||||
|
|
||||||
namespace Baya.Application.Features.Order.Commands;
|
|
||||||
|
|
||||||
public class UpdateUserOrderCommandHandler(IUnitOfWork unitOfWork) : IRequestHandler<UpdateUserOrderCommand,OperationResult<bool>>
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
public async ValueTask<OperationResult<bool>> Handle(UpdateUserOrderCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var order = await unitOfWork.OrderRepository.GetUserOrderByIdAndUserIdAsync(request.UserId, request.OrderId,
|
|
||||||
true);
|
|
||||||
|
|
||||||
if(order is null)
|
|
||||||
return OperationResult<bool>.NotFoundResult("Specified Order not found");
|
|
||||||
|
|
||||||
order.OrderName=request.OrderName;
|
|
||||||
|
|
||||||
await unitOfWork.CommitAsync();
|
|
||||||
|
|
||||||
return OperationResult<bool>.SuccessResult(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
using Baya.Application.Models.Common;
|
|
||||||
using Baya.SharedKernel.ValidationBase;
|
|
||||||
using Baya.SharedKernel.ValidationBase.Contracts;
|
|
||||||
using FluentValidation;
|
|
||||||
using Mediator;
|
|
||||||
|
|
||||||
namespace Baya.Application.Features.Order.Commands;
|
|
||||||
|
|
||||||
public record UpdateUserOrderCommand
|
|
||||||
(int OrderId, string OrderName) : IRequest<OperationResult<bool>>,IValidatableModel<UpdateUserOrderCommand>
|
|
||||||
{
|
|
||||||
[JsonIgnore]
|
|
||||||
public int UserId { get; set; }
|
|
||||||
|
|
||||||
public IValidator<UpdateUserOrderCommand> ValidateApplicationModel(ApplicationBaseValidationModelProvider<UpdateUserOrderCommand> validator)
|
|
||||||
{
|
|
||||||
validator.RuleFor(c => c.OrderId).NotEmpty().GreaterThan(0);
|
|
||||||
validator.RuleFor(c => c.OrderName).NotEmpty().NotNull();
|
|
||||||
|
|
||||||
return validator;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
-6
@@ -1,6 +0,0 @@
|
|||||||
using Baya.Application.Models.Common;
|
|
||||||
using Mediator;
|
|
||||||
|
|
||||||
namespace Baya.Application.Features.Order.Queries.GetAllOrders;
|
|
||||||
|
|
||||||
public record GetAllOrdersQuery():IRequest<OperationResult<List<GetAllOrdersQueryResult>>>;
|
|
||||||
-20
@@ -1,20 +0,0 @@
|
|||||||
using Baya.Application.Contracts.Persistence;
|
|
||||||
using Baya.Application.Models.Common;
|
|
||||||
using MapsterMapper;
|
|
||||||
using Mediator;
|
|
||||||
|
|
||||||
namespace Baya.Application.Features.Order.Queries.GetAllOrders
|
|
||||||
{
|
|
||||||
internal class GetAllOrdersQueryHandler(IUnitOfWork unitOfWork, IMapper mapper)
|
|
||||||
: IRequestHandler<GetAllOrdersQuery, OperationResult<List<GetAllOrdersQueryResult>>>
|
|
||||||
{
|
|
||||||
public async ValueTask<OperationResult<List<GetAllOrdersQueryResult>>> Handle(GetAllOrdersQuery request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var orders = await unitOfWork.OrderRepository.GetAllOrdersWithRelatedUserAsync();
|
|
||||||
|
|
||||||
var result = orders.Select(mapper.Map<Domain.Entities.Order.Order,GetAllOrdersQueryResult>).ToList();
|
|
||||||
|
|
||||||
return OperationResult<List<GetAllOrdersQueryResult>>.SuccessResult(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
using Mapster;
|
|
||||||
|
|
||||||
namespace Baya.Application.Features.Order.Queries.GetAllOrders;
|
|
||||||
|
|
||||||
public record GetAllOrdersQueryResult(int OrderId, string OrderName, int OrderOwnerId, string OrderOwnerUserName);
|
|
||||||
|
|
||||||
class GetAllOrdersQueryResultMapping : IRegister
|
|
||||||
{
|
|
||||||
public void Register(TypeAdapterConfig config)
|
|
||||||
{
|
|
||||||
config.NewConfig<Domain.Entities.Order.Order, GetAllOrdersQueryResult>()
|
|
||||||
.Map(dest => dest.OrderId, src => src.Id)
|
|
||||||
.Map(dest => dest.OrderName, src => src.OrderName)
|
|
||||||
.Map(dest => dest.OrderOwnerId, src => src.User.Id)
|
|
||||||
.Map(dest => dest.OrderOwnerUserName, src => src.User.UserName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
using Baya.Application.Contracts.Persistence;
|
|
||||||
using Baya.Application.Models.Common;
|
|
||||||
using Mediator;
|
|
||||||
|
|
||||||
namespace Baya.Application.Features.Order.Queries.GetUserOrders;
|
|
||||||
|
|
||||||
internal class GetUserOrdersQueryHandler(IUnitOfWork unitOfWork)
|
|
||||||
: IRequestHandler<GetUserOrdersQueryModel, OperationResult<List<GetUsersQueryResultModel>>>
|
|
||||||
{
|
|
||||||
public async ValueTask<OperationResult<List<GetUsersQueryResultModel>>> Handle(GetUserOrdersQueryModel request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var orders = await unitOfWork.OrderRepository.GetAllUserOrdersAsync(request.UserId);
|
|
||||||
|
|
||||||
if(!orders.Any())
|
|
||||||
return OperationResult<List<GetUsersQueryResultModel>>.NotFoundResult("You Don't Have Any Orders");
|
|
||||||
|
|
||||||
var result = orders.Select(c => new GetUsersQueryResultModel(c.Id, c.OrderName));
|
|
||||||
|
|
||||||
return OperationResult<List<GetUsersQueryResultModel>>.SuccessResult(result.ToList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-6
@@ -1,6 +0,0 @@
|
|||||||
using Baya.Application.Models.Common;
|
|
||||||
using Mediator;
|
|
||||||
|
|
||||||
namespace Baya.Application.Features.Order.Queries.GetUserOrders;
|
|
||||||
|
|
||||||
public record GetUserOrdersQueryModel(int UserId) : IRequest<OperationResult<List<GetUsersQueryResultModel>>>;
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
namespace Baya.Application.Features.Order.Queries.GetUserOrders;
|
|
||||||
|
|
||||||
public record GetUsersQueryResultModel(int OrderId, string OrderName);
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using Baya.Application.Contracts.Common;
|
||||||
|
using Baya.Application.Models.Common;
|
||||||
|
using Mediator;
|
||||||
|
|
||||||
|
namespace Baya.Application.Features.System.Queries.Ping;
|
||||||
|
|
||||||
|
internal sealed class PingQueryHandler(IDateTimeProvider dateTimeProvider)
|
||||||
|
: IRequestHandler<PingQuery, OperationResult<PingQueryResult>>
|
||||||
|
{
|
||||||
|
public ValueTask<OperationResult<PingQueryResult>> Handle(PingQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = new PingQueryResult("Baya.Web.Api", "ok", dateTimeProvider.UtcNow);
|
||||||
|
|
||||||
|
return ValueTask.FromResult(OperationResult<PingQueryResult>.SuccessResult(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace Baya.Application.Features.System.Queries.Ping;
|
||||||
|
|
||||||
|
/// <summary>Liveness payload proving the REST → Mediator → envelope pipeline end-to-end.</summary>
|
||||||
|
public record PingQueryResult(string Service, string Status, DateTimeOffset ServerTimeUtc);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
using Baya.Application.Models.Common;
|
||||||
|
using Mediator;
|
||||||
|
|
||||||
|
namespace Baya.Application.Features.System.Queries.Ping;
|
||||||
|
|
||||||
|
public record PingQuery : IRequest<OperationResult<PingQueryResult>>;
|
||||||
@@ -16,8 +16,10 @@ public static class ServiceCollectionExtension
|
|||||||
options.Namespace = "Baya.Application.Mediator";
|
options.Namespace = "Baya.Application.Mediator";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
|
||||||
|
|
||||||
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(MetricsBehaviour<,>));
|
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(MetricsBehaviour<,>));
|
||||||
|
|
||||||
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidateCommandBehavior<,>));
|
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidateCommandBehavior<,>));
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Baya.Domain.Common;
|
namespace Baya.Domain.Common;
|
||||||
|
|
||||||
public interface IEntity
|
public interface IEntity
|
||||||
{
|
{
|
||||||
@@ -6,11 +6,21 @@ public interface IEntity
|
|||||||
|
|
||||||
public interface ITimeModification
|
public interface ITimeModification
|
||||||
{
|
{
|
||||||
DateTime CreatedTime { get; set; }
|
DateTimeOffset CreatedAt { get; set; }
|
||||||
DateTime? ModifiedDate { get; set; }
|
DateTimeOffset? ModifiedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class BaseEntity<TKey> : IEntity, ITimeModification
|
/// <summary>
|
||||||
|
/// An entity whose create/modify timestamps and the acting user are stamped automatically by the
|
||||||
|
/// SaveChanges audit interceptor — handlers must never set these fields themselves.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAuditableEntity : ITimeModification
|
||||||
|
{
|
||||||
|
int? CreatedById { get; set; }
|
||||||
|
int? ModifiedById { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class BaseEntity<TKey> : IEntity, IAuditableEntity
|
||||||
{
|
{
|
||||||
public TKey Id { get; protected set; }
|
public TKey Id { get; protected set; }
|
||||||
|
|
||||||
@@ -49,11 +59,13 @@ public abstract class BaseEntity<TKey> : IEntity, ITimeModification
|
|||||||
return (GetType().ToString() + Id).GetHashCode();
|
return (GetType().ToString() + Id).GetHashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
public DateTime CreatedTime { get; set; }
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
public DateTime? ModifiedDate { get; set; }
|
public DateTimeOffset? ModifiedAt { get; set; }
|
||||||
|
public int? CreatedById { get; set; }
|
||||||
|
public int? ModifiedById { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class BaseEntity : BaseEntity<int>
|
public abstract class BaseEntity : BaseEntity<int>
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
using Baya.Domain.Common;
|
|
||||||
|
|
||||||
namespace Baya.Domain.Entities.Order;
|
|
||||||
|
|
||||||
public class Order:BaseEntity
|
|
||||||
{
|
|
||||||
public string OrderName { get; set; }
|
|
||||||
public bool IsDeleted { get; set; }
|
|
||||||
|
|
||||||
#region Navigation Properties
|
|
||||||
|
|
||||||
public User.User User { get; set; }
|
|
||||||
public int UserId { get; set; }
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@@ -19,11 +19,4 @@ public class User:IdentityUser<int>,IEntity
|
|||||||
public ICollection<UserClaim> Claims { get; set; }
|
public ICollection<UserClaim> Claims { get; set; }
|
||||||
public ICollection<UserToken> Tokens { get; set; }
|
public ICollection<UserToken> Tokens { get; set; }
|
||||||
public ICollection<UserRefreshToken> UserRefreshTokens { get; set; }
|
public ICollection<UserRefreshToken> UserRefreshTokens { get; set; }
|
||||||
|
|
||||||
#region Navigation Properties
|
|
||||||
|
|
||||||
public IList<Order.Order> Orders { get; set; }
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,10 @@
|
|||||||
using Baya.Domain.Common;
|
using Baya.Domain.Common;
|
||||||
|
|
||||||
namespace Baya.Domain.Entities.User;
|
namespace Baya.Domain.Entities.User;
|
||||||
|
|
||||||
public class UserRefreshToken:BaseEntity<Guid>
|
public class UserRefreshToken:BaseEntity<Guid>
|
||||||
{
|
{
|
||||||
public UserRefreshToken()
|
|
||||||
{
|
|
||||||
CreatedAt=DateTime.Now;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
public User User { get; set; }
|
public User User { get; set; }
|
||||||
public DateTime CreatedAt { get; set; }
|
|
||||||
public bool IsValid { get; set; }
|
public bool IsValid { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -17,4 +17,7 @@
|
|||||||
<ProjectReference Include="..\Baya.Infrastructure.Identity\Baya.Infrastructure.Identity.csproj" />
|
<ProjectReference Include="..\Baya.Infrastructure.Identity\Baya.Infrastructure.Identity.csproj" />
|
||||||
<ProjectReference Include="..\Baya.Infrastructure.Persistence\Baya.Infrastructure.Persistence.csproj" />
|
<ProjectReference Include="..\Baya.Infrastructure.Persistence\Baya.Infrastructure.Persistence.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Baya.Application.Contracts.Common;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Baya.Infrastructure.CrossCutting.Seams;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Local-disk implementation of <see cref="IObjectStorage"/> — the mock seam. Blobs are stored as
|
||||||
|
/// files under a configured scratch root, keyed by an opaque storage key. The real implementation
|
||||||
|
/// swaps to MinIO/S3/ArvanCloud (presigned URLs) behind the same interface.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LocalDiskObjectStorage : IObjectStorage
|
||||||
|
{
|
||||||
|
private readonly string _root;
|
||||||
|
|
||||||
|
public LocalDiskObjectStorage(IOptions<SeamOptions> options)
|
||||||
|
{
|
||||||
|
var configured = options.Value.ObjectStorage.RootPath;
|
||||||
|
_root = string.IsNullOrWhiteSpace(configured)
|
||||||
|
? Path.Combine(Path.GetTempPath(), "balinyaar-object-storage")
|
||||||
|
: configured;
|
||||||
|
|
||||||
|
Directory.CreateDirectory(_root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask PutAsync(string key, Stream content, string contentType, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var path = ResolvePath(key);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||||
|
|
||||||
|
await using var file = File.Create(path);
|
||||||
|
await content.CopyToAsync(file, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<Stream?> GetAsync(string key, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var path = ResolvePath(key);
|
||||||
|
Stream? stream = File.Exists(path) ? File.OpenRead(path) : null;
|
||||||
|
return ValueTask.FromResult(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask DeleteAsync(string key, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var path = ResolvePath(key);
|
||||||
|
if (File.Exists(path))
|
||||||
|
File.Delete(path);
|
||||||
|
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetUrl(string key) => new Uri(Path.Combine(_root, SanitizeKey(key))).AbsoluteUri;
|
||||||
|
|
||||||
|
private string ResolvePath(string key) => Path.Combine(_root, SanitizeKey(key));
|
||||||
|
|
||||||
|
// Keep the key from escaping the storage root; treat '/' as a folder separator only.
|
||||||
|
private static string SanitizeKey(string key)
|
||||||
|
{
|
||||||
|
var normalized = key.Replace('\\', '/').TrimStart('/');
|
||||||
|
var segments = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Where(s => s != "." && s != "..");
|
||||||
|
return Path.Combine([.. segments]);
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
using Baya.Application.Contracts.Common;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Baya.Infrastructure.CrossCutting.Seams;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No-op implementation of <see cref="INotificationDispatcher"/> — the mock seam. It logs that a
|
||||||
|
/// notification would be sent (no PII in the log). The real in-app write lands in backend-phase-15,
|
||||||
|
/// with SMS/push channels added behind the same interface.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LogNotificationDispatcher(ILogger<LogNotificationDispatcher> logger) : INotificationDispatcher
|
||||||
|
{
|
||||||
|
public ValueTask DispatchAsync(Notification notification, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"Notification suppressed (mock dispatcher): channel {Channel} to user {UserId}",
|
||||||
|
notification.Channel,
|
||||||
|
notification.RecipientUserId);
|
||||||
|
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Baya.Application.Contracts.Common;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
|
namespace Baya.Infrastructure.CrossCutting.Seams;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// In-process <see cref="IMemoryCache"/> implementation of <see cref="ICacheService"/> — the mock seam.
|
||||||
|
/// The real implementation swaps to Redis (StackExchange.Redis) while keeping the same key/TTL scheme.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MemoryCacheService(IMemoryCache cache) : ICacheService
|
||||||
|
{
|
||||||
|
public ValueTask<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return ValueTask.FromResult(cache.TryGetValue(key, out T? value) ? value : default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask SetAsync<T>(string key, T value, TimeSpan? ttl = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var options = new MemoryCacheEntryOptions();
|
||||||
|
if (ttl is { } expiry)
|
||||||
|
options.AbsoluteExpirationRelativeToNow = expiry;
|
||||||
|
|
||||||
|
cache.Set(key, value, options);
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask RemoveAsync(string key, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cache.Remove(key);
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<T> GetOrCreateAsync<T>(
|
||||||
|
string key,
|
||||||
|
Func<CancellationToken, ValueTask<T>> factory,
|
||||||
|
TimeSpan? ttl = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (cache.TryGetValue(key, out T? cached) && cached is not null)
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
var value = await factory(cancellationToken);
|
||||||
|
await SetAsync(key, value, ttl, cancellationToken);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
namespace Baya.Infrastructure.CrossCutting.Seams;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Options bound from the <c>Seams</c> configuration section. The mock seams read non-secret defaults
|
||||||
|
/// from here; production keys/paths come from environment variables or user-secrets, never committed.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SeamOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "Seams";
|
||||||
|
|
||||||
|
public FieldEncryptionOptions FieldEncryption { get; set; } = new();
|
||||||
|
public ObjectStorageOptions ObjectStorage { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FieldEncryptionOptions
|
||||||
|
{
|
||||||
|
/// <summary>Base64 32-byte AES key. Local-dev default only; override per environment.</summary>
|
||||||
|
public string Key { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Base64 HMAC key used for deterministic lookup hashes.</summary>
|
||||||
|
public string HashKey { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ObjectStorageOptions
|
||||||
|
{
|
||||||
|
/// <summary>Filesystem root the local-disk mock writes blobs under.</summary>
|
||||||
|
public string RootPath { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Baya.Application.Contracts.Common;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Baya.Infrastructure.CrossCutting.Seams;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Local symmetric-key implementation of <see cref="IFieldEncryptor"/> — AES-256-CBC with a random
|
||||||
|
/// per-value IV (prepended to the ciphertext) for at-rest reversibility, and a keyed HMAC-SHA256 for
|
||||||
|
/// deterministic lookup hashes. This is the mock seam: the real implementation swaps to a KMS / Key
|
||||||
|
/// Vault provider behind the same interface. Plaintext is never logged.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SymmetricFieldEncryptor : IFieldEncryptor
|
||||||
|
{
|
||||||
|
private readonly byte[] _key;
|
||||||
|
private readonly byte[] _hashKey;
|
||||||
|
|
||||||
|
public SymmetricFieldEncryptor(IOptions<SeamOptions> options)
|
||||||
|
{
|
||||||
|
var settings = options.Value.FieldEncryption;
|
||||||
|
|
||||||
|
// Derive a stable 32-byte AES key from whatever the operator configured (any length/format),
|
||||||
|
// so a human-friendly secret still yields a valid key. SHA-256 of the configured material.
|
||||||
|
_key = SHA256.HashData(Encoding.UTF8.GetBytes(Require(settings.Key, nameof(settings.Key))));
|
||||||
|
|
||||||
|
var hashMaterial = string.IsNullOrEmpty(settings.HashKey) ? settings.Key : settings.HashKey;
|
||||||
|
_hashKey = SHA256.HashData(Encoding.UTF8.GetBytes(Require(hashMaterial, nameof(settings.HashKey))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Encrypt(string plaintext)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(plaintext))
|
||||||
|
return plaintext;
|
||||||
|
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Key = _key;
|
||||||
|
aes.GenerateIV();
|
||||||
|
|
||||||
|
using var encryptor = aes.CreateEncryptor();
|
||||||
|
var plainBytes = Encoding.UTF8.GetBytes(plaintext);
|
||||||
|
var cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
|
||||||
|
|
||||||
|
var result = new byte[aes.IV.Length + cipherBytes.Length];
|
||||||
|
Buffer.BlockCopy(aes.IV, 0, result, 0, aes.IV.Length);
|
||||||
|
Buffer.BlockCopy(cipherBytes, 0, result, aes.IV.Length, cipherBytes.Length);
|
||||||
|
|
||||||
|
return Convert.ToBase64String(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Decrypt(string ciphertext)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(ciphertext))
|
||||||
|
return ciphertext;
|
||||||
|
|
||||||
|
var cipherWithIv = Convert.FromBase64String(ciphertext);
|
||||||
|
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Key = _key;
|
||||||
|
|
||||||
|
var ivLength = aes.BlockSize / 8;
|
||||||
|
var iv = new byte[ivLength];
|
||||||
|
Buffer.BlockCopy(cipherWithIv, 0, iv, 0, ivLength);
|
||||||
|
aes.IV = iv;
|
||||||
|
|
||||||
|
using var decryptor = aes.CreateDecryptor();
|
||||||
|
var cipherBytes = decryptor.TransformFinalBlock(cipherWithIv, ivLength, cipherWithIv.Length - ivLength);
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetString(cipherBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Hash(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return value;
|
||||||
|
|
||||||
|
using var hmac = new HMACSHA256(_hashKey);
|
||||||
|
var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(value));
|
||||||
|
return Convert.ToHexString(hashBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Require(string value, string name) =>
|
||||||
|
string.IsNullOrWhiteSpace(value)
|
||||||
|
? throw new InvalidOperationException($"Seams:FieldEncryption:{name} must be configured.")
|
||||||
|
: value;
|
||||||
|
}
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
using Baya.Application.Contracts.Common;
|
||||||
|
|
||||||
|
namespace Baya.Infrastructure.CrossCutting.Seams;
|
||||||
|
|
||||||
|
/// <summary>Real system clock. Tests substitute <see cref="IDateTimeProvider"/> to freeze time.</summary>
|
||||||
|
public sealed class SystemDateTimeProvider : IDateTimeProvider
|
||||||
|
{
|
||||||
|
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
using Baya.Application.Contracts.Common;
|
||||||
|
using Baya.Infrastructure.CrossCutting.Seams;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Baya.Infrastructure.CrossCutting.ServiceConfiguration;
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtension
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers the cross-cutting seams (time, PII encryption, cache, object storage, notifications)
|
||||||
|
/// with their in-memory/local mock implementations. Swapping in a real provider later is a
|
||||||
|
/// registration change here — callers depend only on the Application contracts.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddCrossCuttingSeams(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
services.Configure<SeamOptions>(configuration.GetSection(SeamOptions.SectionName));
|
||||||
|
|
||||||
|
services.AddMemoryCache();
|
||||||
|
|
||||||
|
services.AddSingleton<IDateTimeProvider, SystemDateTimeProvider>();
|
||||||
|
services.AddSingleton<IFieldEncryptor, SymmetricFieldEncryptor>();
|
||||||
|
services.AddSingleton<ICacheService, MemoryCacheService>();
|
||||||
|
services.AddSingleton<IObjectStorage, LocalDiskObjectStorage>();
|
||||||
|
services.AddScoped<INotificationDispatcher, LogNotificationDispatcher>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Baya.Application.Contracts.Common;
|
||||||
|
using Baya.SharedKernel.Extensions;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Baya.Infrastructure.Identity.Identity.CurrentUser;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves the current user from the active HTTP request's claims. When there is no request or no
|
||||||
|
/// authenticated principal (background work, startup), all members report "no user".
|
||||||
|
/// </summary>
|
||||||
|
public sealed class HttpContextCurrentUser(IHttpContextAccessor httpContextAccessor) : ICurrentUser
|
||||||
|
{
|
||||||
|
private ClaimsPrincipal? Principal => httpContextAccessor.HttpContext?.User;
|
||||||
|
|
||||||
|
public int? UserId
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var rawId = Principal?.Identity?.GetUserId();
|
||||||
|
return rawId.HasValue() && int.TryParse(rawId, out var id) ? id : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsAuthenticated => Principal?.Identity?.IsAuthenticated ?? false;
|
||||||
|
|
||||||
|
public IReadOnlyList<string> Roles =>
|
||||||
|
Principal?.FindAll(ClaimTypes.Role).Select(c => c.Value).ToArray() ?? [];
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
using Baya.Application.Contracts.Common;
|
||||||
|
|
||||||
|
namespace Baya.Infrastructure.Identity.Identity.CurrentUser;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Null-object <see cref="ICurrentUser"/> for non-HTTP contexts (background jobs, tests) — reports an
|
||||||
|
/// unauthenticated caller so audit stamping leaves the user fields null rather than failing.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NullCurrentUser : ICurrentUser
|
||||||
|
{
|
||||||
|
public int? UserId => null;
|
||||||
|
|
||||||
|
public bool IsAuthenticated => false;
|
||||||
|
|
||||||
|
public IReadOnlyList<string> Roles => [];
|
||||||
|
}
|
||||||
+5
@@ -1,10 +1,12 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Baya.Application.Contracts;
|
using Baya.Application.Contracts;
|
||||||
|
using Baya.Application.Contracts.Common;
|
||||||
using Baya.Application.Contracts.Identity;
|
using Baya.Application.Contracts.Identity;
|
||||||
using Baya.Application.Models.ApiResult;
|
using Baya.Application.Models.ApiResult;
|
||||||
using Baya.Domain.Entities.User;
|
using Baya.Domain.Entities.User;
|
||||||
using Baya.Infrastructure.Identity.Identity;
|
using Baya.Infrastructure.Identity.Identity;
|
||||||
|
using Baya.Infrastructure.Identity.Identity.CurrentUser;
|
||||||
using Baya.Infrastructure.Identity.Identity.Dtos;
|
using Baya.Infrastructure.Identity.Identity.Dtos;
|
||||||
using Baya.Infrastructure.Identity.Identity.Extensions;
|
using Baya.Infrastructure.Identity.Identity.Extensions;
|
||||||
using Baya.Infrastructure.Identity.Identity.Manager;
|
using Baya.Infrastructure.Identity.Identity.Manager;
|
||||||
@@ -29,6 +31,9 @@ public static class ServiceCollectionExtension
|
|||||||
{
|
{
|
||||||
public static IServiceCollection RegisterIdentityServices(this IServiceCollection services,IdentitySettings identitySettings)
|
public static IServiceCollection RegisterIdentityServices(this IServiceCollection services,IdentitySettings identitySettings)
|
||||||
{
|
{
|
||||||
|
services.AddHttpContextAccessor();
|
||||||
|
services.AddScoped<ICurrentUser, HttpContextCurrentUser>();
|
||||||
|
|
||||||
services.AddScoped<IJwtService, JwtService>();
|
services.AddScoped<IJwtService, JwtService>();
|
||||||
services.AddScoped<IAppUserManager, AppUserManagerImplementation>();
|
services.AddScoped<IAppUserManager, AppUserManagerImplementation>();
|
||||||
services.AddScoped<ISeedDataBase, SeedDataBase>();
|
services.AddScoped<ISeedDataBase, SeedDataBase>();
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ public class ApplicationDbContext: IdentityDbContext<User, Role, int, UserClaim,
|
|||||||
private void OnSavingChanges(object sender, SavingChangesEventArgs e)
|
private void OnSavingChanges(object sender, SavingChangesEventArgs e)
|
||||||
{
|
{
|
||||||
_cleanString();
|
_cleanString();
|
||||||
ConfigureEntityDates();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void _cleanString()
|
private void _cleanString()
|
||||||
@@ -62,30 +61,4 @@ public class ApplicationDbContext: IdentityDbContext<User, Role, int, UserClaim,
|
|||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ConfigureEntityDates()
|
|
||||||
{
|
|
||||||
var updatedEntities = ChangeTracker.Entries().Where(x =>
|
|
||||||
x.Entity is ITimeModification && x.State == EntityState.Modified).Select(x => x.Entity as ITimeModification);
|
|
||||||
|
|
||||||
var addedEntities = ChangeTracker.Entries().Where(x =>
|
|
||||||
x.Entity is ITimeModification && x.State == EntityState.Added).Select(x => x.Entity as ITimeModification);
|
|
||||||
|
|
||||||
foreach (var entity in updatedEntities)
|
|
||||||
{
|
|
||||||
if (entity != null)
|
|
||||||
{
|
|
||||||
entity.ModifiedDate = DateTime.Now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var entity in addedEntities)
|
|
||||||
{
|
|
||||||
if (entity != null)
|
|
||||||
{
|
|
||||||
entity.CreatedTime = DateTime.Now;
|
|
||||||
entity.ModifiedDate = DateTime.Now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
using Baya.Domain.Entities.Order;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
||||||
|
|
||||||
namespace Baya.Infrastructure.Persistence.Configuration.OrderConfig;
|
|
||||||
|
|
||||||
internal class OrderConfig:IEntityTypeConfiguration<Order>
|
|
||||||
{
|
|
||||||
public void Configure(EntityTypeBuilder<Order> builder)
|
|
||||||
{
|
|
||||||
builder.HasOne(c => c.User).WithMany(c => c.Orders).HasForeignKey(c => c.UserId);
|
|
||||||
|
|
||||||
builder.HasQueryFilter(c => !c.IsDeleted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Baya.Application.Contracts.Common;
|
||||||
|
using Baya.Domain.Common;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||||
|
|
||||||
|
namespace Baya.Infrastructure.Persistence.Interceptors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stamps audit fields on every save: <c>CreatedAt</c>/<c>CreatedById</c> on insert and
|
||||||
|
/// <c>ModifiedAt</c>/<c>ModifiedById</c> on update, sourcing time from <see cref="IDateTimeProvider"/>
|
||||||
|
/// and the acting user from <see cref="ICurrentUser"/>. Handlers never set these fields.
|
||||||
|
/// This is the extension point backend-phase-1 builds on to also write append-only audit-log rows.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AuditFieldInterceptor(ICurrentUser currentUser, IDateTimeProvider dateTimeProvider)
|
||||||
|
: SaveChangesInterceptor
|
||||||
|
{
|
||||||
|
public override InterceptionResult<int> SavingChanges(
|
||||||
|
DbContextEventData eventData,
|
||||||
|
InterceptionResult<int> result)
|
||||||
|
{
|
||||||
|
Stamp(eventData.Context);
|
||||||
|
return base.SavingChanges(eventData, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
|
||||||
|
DbContextEventData eventData,
|
||||||
|
InterceptionResult<int> result,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Stamp(eventData.Context);
|
||||||
|
return base.SavingChangesAsync(eventData, result, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Stamp(DbContext? context)
|
||||||
|
{
|
||||||
|
if (context is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var now = dateTimeProvider.UtcNow;
|
||||||
|
var userId = currentUser.UserId;
|
||||||
|
|
||||||
|
foreach (var entry in context.ChangeTracker.Entries<ITimeModification>())
|
||||||
|
{
|
||||||
|
switch (entry.State)
|
||||||
|
{
|
||||||
|
case EntityState.Added:
|
||||||
|
entry.Entity.CreatedAt = now;
|
||||||
|
entry.Entity.ModifiedAt = now;
|
||||||
|
if (entry.Entity is IAuditableEntity addedAuditable)
|
||||||
|
{
|
||||||
|
addedAuditable.CreatedById = userId;
|
||||||
|
addedAuditable.ModifiedById = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityState.Modified:
|
||||||
|
entry.Entity.ModifiedAt = now;
|
||||||
|
if (entry.Entity is IAuditableEntity modifiedAuditable)
|
||||||
|
modifiedAuditable.ModifiedById = userId;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20210327210004_Init.Designer.cs
Generated
-374
@@ -1,374 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Baya.Infrastructure.Persistence;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Metadata;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Persistence;
|
|
||||||
|
|
||||||
namespace Persistence.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
|
||||||
[Migration("20210327210004_Init")]
|
|
||||||
partial class Init
|
|
||||||
{
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.UseIdentityColumns()
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 128)
|
|
||||||
.HasAnnotation("ProductVersion", "5.0.0");
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.Role", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int")
|
|
||||||
.UseIdentityColumn();
|
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedDate")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<string>("DisplayName")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("nvarchar(256)");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("nvarchar(256)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedName")
|
|
||||||
.IsUnique()
|
|
||||||
.HasDatabaseName("RoleNameIndex")
|
|
||||||
.HasFilter("[NormalizedName] IS NOT NULL");
|
|
||||||
|
|
||||||
b.ToTable("Roles", "usr");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.RoleClaim", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int")
|
|
||||||
.UseIdentityColumn();
|
|
||||||
|
|
||||||
b.Property<string>("ClaimType")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimValue")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedClaim")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<int>("RoleId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
|
||||||
|
|
||||||
b.ToTable("RoleClaims", "usr");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.User", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int")
|
|
||||||
.HasColumnName("UserId")
|
|
||||||
.UseIdentityColumn();
|
|
||||||
|
|
||||||
b.Property<int>("AccessFailedCount")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("Email")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("nvarchar(256)");
|
|
||||||
|
|
||||||
b.Property<bool>("EmailConfirmed")
|
|
||||||
.HasColumnType("bit");
|
|
||||||
|
|
||||||
b.Property<string>("FamilyName")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("GeneratedCode")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<bool>("LockoutEnabled")
|
|
||||||
.HasColumnType("bit");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
|
||||||
.HasColumnType("datetimeoffset");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedEmail")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("nvarchar(256)");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedUserName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("nvarchar(256)");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("PhoneNumber")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<bool>("PhoneNumberConfirmed")
|
|
||||||
.HasColumnType("bit");
|
|
||||||
|
|
||||||
b.Property<string>("SecurityStamp")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<bool>("TwoFactorEnabled")
|
|
||||||
.HasColumnType("bit");
|
|
||||||
|
|
||||||
b.Property<string>("UserName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("nvarchar(256)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedEmail")
|
|
||||||
.HasDatabaseName("EmailIndex");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedUserName")
|
|
||||||
.IsUnique()
|
|
||||||
.HasDatabaseName("UserNameIndex")
|
|
||||||
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
|
||||||
|
|
||||||
b.ToTable("Users", "usr");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserClaim", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int")
|
|
||||||
.UseIdentityColumn();
|
|
||||||
|
|
||||||
b.Property<string>("ClaimType")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimValue")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("UserClaims", "usr");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserLogin", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LoginProvider")
|
|
||||||
.HasColumnType("nvarchar(450)");
|
|
||||||
|
|
||||||
b.Property<string>("ProviderKey")
|
|
||||||
.HasColumnType("nvarchar(450)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("LoggedOn")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<string>("ProviderDisplayName")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.HasKey("LoginProvider", "ProviderKey");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("UserLogins", "usr");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserRefreshToken", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uniqueidentifier");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedTime")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<bool>("IsValid")
|
|
||||||
.HasColumnType("bit");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("ModifiedDate")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("UserRefreshTokens", "usr");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserRole", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<int>("RoleId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedUserRoleDate")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "RoleId");
|
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
|
||||||
|
|
||||||
b.ToTable("UserRoles", "usr");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserToken", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("LoginProvider")
|
|
||||||
.HasColumnType("nvarchar(450)");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasColumnType("nvarchar(450)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("GeneratedTime")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<string>("Value")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "LoginProvider", "Name");
|
|
||||||
|
|
||||||
b.ToTable("UserTokens", "usr");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.RoleClaim", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Domain.Entities.User.Role", "Role")
|
|
||||||
.WithMany("Claims")
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Role");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserClaim", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Domain.Entities.User.User", "User")
|
|
||||||
.WithMany("Claims")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserLogin", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Domain.Entities.User.User", "User")
|
|
||||||
.WithMany("Logins")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserRefreshToken", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Domain.Entities.User.User", "User")
|
|
||||||
.WithMany("UserRefreshTokens")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserRole", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Domain.Entities.User.Role", "Role")
|
|
||||||
.WithMany("Users")
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Domain.Entities.User.User", "User")
|
|
||||||
.WithMany("UserRoles")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Role");
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserToken", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Domain.Entities.User.User", "User")
|
|
||||||
.WithMany("Tokens")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.Role", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Claims");
|
|
||||||
|
|
||||||
b.Navigation("Users");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.User", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Claims");
|
|
||||||
|
|
||||||
b.Navigation("Logins");
|
|
||||||
|
|
||||||
b.Navigation("Tokens");
|
|
||||||
|
|
||||||
b.Navigation("UserRefreshTokens");
|
|
||||||
|
|
||||||
b.Navigation("UserRoles");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-422
@@ -1,422 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Baya.Infrastructure.Persistence;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Metadata;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Persistence;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Persistence.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
|
||||||
[Migration("20221205084354_AddedOrderAndUserRelation")]
|
|
||||||
partial class AddedOrderAndUserRelation
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "7.0.0")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
|
||||||
|
|
||||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.Order.Order", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedTime")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("ModifiedDate")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<string>("OrderName")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("Orders");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.Role", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedDate")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<string>("DisplayName")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("nvarchar(256)");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("nvarchar(256)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedName")
|
|
||||||
.IsUnique()
|
|
||||||
.HasDatabaseName("RoleNameIndex")
|
|
||||||
.HasFilter("[NormalizedName] IS NOT NULL");
|
|
||||||
|
|
||||||
b.ToTable("Roles", "usr");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.RoleClaim", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("ClaimType")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimValue")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedClaim")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<int>("RoleId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
|
||||||
|
|
||||||
b.ToTable("RoleClaims", "usr");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.User", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int")
|
|
||||||
.HasColumnName("UserId");
|
|
||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<int>("AccessFailedCount")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("Email")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("nvarchar(256)");
|
|
||||||
|
|
||||||
b.Property<bool>("EmailConfirmed")
|
|
||||||
.HasColumnType("bit");
|
|
||||||
|
|
||||||
b.Property<string>("FamilyName")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("GeneratedCode")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<bool>("LockoutEnabled")
|
|
||||||
.HasColumnType("bit");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
|
||||||
.HasColumnType("datetimeoffset");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedEmail")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("nvarchar(256)");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedUserName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("nvarchar(256)");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("PhoneNumber")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<bool>("PhoneNumberConfirmed")
|
|
||||||
.HasColumnType("bit");
|
|
||||||
|
|
||||||
b.Property<string>("SecurityStamp")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<bool>("TwoFactorEnabled")
|
|
||||||
.HasColumnType("bit");
|
|
||||||
|
|
||||||
b.Property<string>("UserName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("nvarchar(256)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedEmail")
|
|
||||||
.HasDatabaseName("EmailIndex");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedUserName")
|
|
||||||
.IsUnique()
|
|
||||||
.HasDatabaseName("UserNameIndex")
|
|
||||||
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
|
||||||
|
|
||||||
b.ToTable("Users", "usr");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserClaim", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("ClaimType")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimValue")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("UserClaims", "usr");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserLogin", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LoginProvider")
|
|
||||||
.HasColumnType("nvarchar(450)");
|
|
||||||
|
|
||||||
b.Property<string>("ProviderKey")
|
|
||||||
.HasColumnType("nvarchar(450)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("LoggedOn")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<string>("ProviderDisplayName")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.HasKey("LoginProvider", "ProviderKey");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("UserLogins", "usr");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserRefreshToken", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uniqueidentifier");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedTime")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<bool>("IsValid")
|
|
||||||
.HasColumnType("bit");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("ModifiedDate")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("UserRefreshTokens", "usr");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserRole", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<int>("RoleId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedUserRoleDate")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "RoleId");
|
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
|
||||||
|
|
||||||
b.ToTable("UserRoles", "usr");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserToken", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("LoginProvider")
|
|
||||||
.HasColumnType("nvarchar(450)");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasColumnType("nvarchar(450)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("GeneratedTime")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<string>("Value")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "LoginProvider", "Name");
|
|
||||||
|
|
||||||
b.ToTable("UserTokens", "usr");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.Order.Order", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Domain.Entities.User.User", "User")
|
|
||||||
.WithMany("Orders")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.RoleClaim", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Domain.Entities.User.Role", "Role")
|
|
||||||
.WithMany("Claims")
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Role");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserClaim", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Domain.Entities.User.User", "User")
|
|
||||||
.WithMany("Claims")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserLogin", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Domain.Entities.User.User", "User")
|
|
||||||
.WithMany("Logins")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserRefreshToken", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Domain.Entities.User.User", "User")
|
|
||||||
.WithMany("UserRefreshTokens")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserRole", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Domain.Entities.User.Role", "Role")
|
|
||||||
.WithMany("Users")
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Domain.Entities.User.User", "User")
|
|
||||||
.WithMany("UserRoles")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Role");
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.UserToken", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Domain.Entities.User.User", "User")
|
|
||||||
.WithMany("Tokens")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.Role", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Claims");
|
|
||||||
|
|
||||||
b.Navigation("Users");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Domain.Entities.User.User", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Claims");
|
|
||||||
|
|
||||||
b.Navigation("Logins");
|
|
||||||
|
|
||||||
b.Navigation("Orders");
|
|
||||||
|
|
||||||
b.Navigation("Tokens");
|
|
||||||
|
|
||||||
b.Navigation("UserRefreshTokens");
|
|
||||||
|
|
||||||
b.Navigation("UserRoles");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-50
@@ -1,50 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Persistence.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddedOrderAndUserRelation : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Orders",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "int", nullable: false)
|
|
||||||
.Annotation("SqlServer:Identity", "1, 1"),
|
|
||||||
OrderName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
|
||||||
UserId = table.Column<int>(type: "int", nullable: false),
|
|
||||||
CreatedTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
|
||||||
ModifiedDate = table.Column<DateTime>(type: "datetime2", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Orders", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Orders_Users_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalSchema: "usr",
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "UserId",
|
|
||||||
onDelete: ReferentialAction.Restrict);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Orders_UserId",
|
|
||||||
table: "Orders",
|
|
||||||
column: "UserId");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Orders");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-29
@@ -1,29 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Persistence.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddedOrderDeleteFlag : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<bool>(
|
|
||||||
name: "IsDeleted",
|
|
||||||
table: "Orders",
|
|
||||||
type: "bit",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "IsDeleted",
|
|
||||||
table: "Orders");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+13
-53
@@ -9,52 +9,22 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Persistence.Migrations
|
namespace Baya.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
[Migration("20231126140035_AddedOrderDeleteFlag")]
|
[Migration("20260628191947_InitialBaseline")]
|
||||||
partial class AddedOrderDeleteFlag
|
partial class InitialBaseline
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "8.0.0")
|
.HasAnnotation("ProductVersion", "10.0.9")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.Entity("Baya.Domain.Entities.Order.Order", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedTime")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<bool>("IsDeleted")
|
|
||||||
.HasColumnType("bit");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("ModifiedDate")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<string>("OrderName")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("Orders");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Baya.Domain.Entities.User.Role", b =>
|
modelBuilder.Entity("Baya.Domain.Entities.User.Role", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -250,17 +220,20 @@ namespace Persistence.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedTime")
|
b.Property<int?>("CreatedById")
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<bool>("IsValid")
|
b.Property<bool>("IsValid")
|
||||||
.HasColumnType("bit");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
b.Property<DateTime?>("ModifiedDate")
|
b.Property<DateTimeOffset?>("ModifiedAt")
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<int?>("ModifiedById")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
b.Property<int>("UserId")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
@@ -312,17 +285,6 @@ namespace Persistence.Migrations
|
|||||||
b.ToTable("UserTokens", "usr");
|
b.ToTable("UserTokens", "usr");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Baya.Domain.Entities.Order.Order", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Baya.Domain.Entities.User.User", "User")
|
|
||||||
.WithMany("Orders")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Baya.Domain.Entities.User.RoleClaim", b =>
|
modelBuilder.Entity("Baya.Domain.Entities.User.RoleClaim", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Baya.Domain.Entities.User.Role", "Role")
|
b.HasOne("Baya.Domain.Entities.User.Role", "Role")
|
||||||
@@ -410,8 +372,6 @@ namespace Persistence.Migrations
|
|||||||
|
|
||||||
b.Navigation("Logins");
|
b.Navigation("Logins");
|
||||||
|
|
||||||
b.Navigation("Orders");
|
|
||||||
|
|
||||||
b.Navigation("Tokens");
|
b.Navigation("Tokens");
|
||||||
|
|
||||||
b.Navigation("UserRefreshTokens");
|
b.Navigation("UserRefreshTokens");
|
||||||
+11
-5
@@ -1,10 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
namespace Persistence.Migrations
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Baya.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
public partial class Init : Migration
|
/// <inheritdoc />
|
||||||
|
public partial class InitialBaseline : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.EnsureSchema(
|
migrationBuilder.EnsureSchema(
|
||||||
@@ -135,10 +139,11 @@ namespace Persistence.Migrations
|
|||||||
{
|
{
|
||||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
UserId = table.Column<int>(type: "int", nullable: false),
|
UserId = table.Column<int>(type: "int", nullable: false),
|
||||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
|
||||||
IsValid = table.Column<bool>(type: "bit", nullable: false),
|
IsValid = table.Column<bool>(type: "bit", nullable: false),
|
||||||
CreatedTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||||
ModifiedDate = table.Column<DateTime>(type: "datetime2", nullable: true)
|
ModifiedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
|
||||||
|
CreatedById = table.Column<int>(type: "int", nullable: true),
|
||||||
|
ModifiedById = table.Column<int>(type: "int", nullable: true)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
@@ -256,6 +261,7 @@ namespace Persistence.Migrations
|
|||||||
filter: "[NormalizedUserName] IS NOT NULL");
|
filter: "[NormalizedUserName] IS NOT NULL");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
+11
-51
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Persistence.Migrations
|
namespace Baya.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||||
@@ -17,41 +17,11 @@ namespace Persistence.Migrations
|
|||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "10.0.0")
|
.HasAnnotation("ProductVersion", "10.0.9")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.Entity("Baya.Domain.Entities.Order.Order", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedTime")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<bool>("IsDeleted")
|
|
||||||
.HasColumnType("bit");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("ModifiedDate")
|
|
||||||
.HasColumnType("datetime2");
|
|
||||||
|
|
||||||
b.Property<string>("OrderName")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("Orders", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Baya.Domain.Entities.User.Role", b =>
|
modelBuilder.Entity("Baya.Domain.Entities.User.Role", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -247,17 +217,20 @@ namespace Persistence.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedTime")
|
b.Property<int?>("CreatedById")
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<bool>("IsValid")
|
b.Property<bool>("IsValid")
|
||||||
.HasColumnType("bit");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
b.Property<DateTime?>("ModifiedDate")
|
b.Property<DateTimeOffset?>("ModifiedAt")
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<int?>("ModifiedById")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
b.Property<int>("UserId")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
@@ -309,17 +282,6 @@ namespace Persistence.Migrations
|
|||||||
b.ToTable("UserTokens", "usr");
|
b.ToTable("UserTokens", "usr");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Baya.Domain.Entities.Order.Order", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Baya.Domain.Entities.User.User", "User")
|
|
||||||
.WithMany("Orders")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Baya.Domain.Entities.User.RoleClaim", b =>
|
modelBuilder.Entity("Baya.Domain.Entities.User.RoleClaim", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Baya.Domain.Entities.User.Role", "Role")
|
b.HasOne("Baya.Domain.Entities.User.Role", "Role")
|
||||||
@@ -407,8 +369,6 @@ namespace Persistence.Migrations
|
|||||||
|
|
||||||
b.Navigation("Logins");
|
b.Navigation("Logins");
|
||||||
|
|
||||||
b.Navigation("Orders");
|
|
||||||
|
|
||||||
b.Navigation("Tokens");
|
b.Navigation("Tokens");
|
||||||
|
|
||||||
b.Navigation("UserRefreshTokens");
|
b.Navigation("UserRefreshTokens");
|
||||||
|
|||||||
-2
@@ -7,13 +7,11 @@ public class UnitOfWork : IUnitOfWork
|
|||||||
private readonly ApplicationDbContext _db;
|
private readonly ApplicationDbContext _db;
|
||||||
|
|
||||||
public IUserRefreshTokenRepository UserRefreshTokenRepository { get; }
|
public IUserRefreshTokenRepository UserRefreshTokenRepository { get; }
|
||||||
public IOrderRepository OrderRepository { get; }
|
|
||||||
|
|
||||||
public UnitOfWork(ApplicationDbContext db)
|
public UnitOfWork(ApplicationDbContext db)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
UserRefreshTokenRepository = new UserRefreshTokenRepository(_db);
|
UserRefreshTokenRepository = new UserRefreshTokenRepository(_db);
|
||||||
OrderRepository= new OrderRepository(_db);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task CommitAsync()
|
public Task CommitAsync()
|
||||||
|
|||||||
-41
@@ -1,41 +0,0 @@
|
|||||||
using Baya.Application.Contracts.Persistence;
|
|
||||||
using Baya.Domain.Entities.Order;
|
|
||||||
using Baya.Infrastructure.Persistence.Repositories.Common;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Baya.Infrastructure.Persistence.Repositories;
|
|
||||||
|
|
||||||
internal class OrderRepository(ApplicationDbContext dbContext) : BaseAsyncRepository<Order>(dbContext), IOrderRepository
|
|
||||||
{
|
|
||||||
public async Task AddOrderAsync(Order order)
|
|
||||||
{
|
|
||||||
await base.AddAsync(order);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<Order>> GetAllUserOrdersAsync(int userId)
|
|
||||||
{
|
|
||||||
return await base.TableNoTracking.Where(c => c.UserId == userId).ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<Order>> GetAllOrdersWithRelatedUserAsync()
|
|
||||||
{
|
|
||||||
var orders = await base.TableNoTracking.Include(c => c.User).ToListAsync();
|
|
||||||
|
|
||||||
return orders;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Order> GetUserOrderByIdAndUserIdAsync(int userId, int orderId,bool trackEntity)
|
|
||||||
{
|
|
||||||
var order = await base.TableNoTracking.FirstOrDefaultAsync(c => c.UserId == userId && c.Id == orderId);
|
|
||||||
|
|
||||||
if (order is not null && trackEntity)
|
|
||||||
base.DbContext.Attach(order);
|
|
||||||
|
|
||||||
return order;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteUserOrdersAsync(int userId)
|
|
||||||
{
|
|
||||||
await UpdateAsync(c => c.UserId == userId, p => p.SetProperty(order => order.IsDeleted, true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+6
-2
@@ -1,4 +1,5 @@
|
|||||||
using Baya.Application.Contracts.Persistence;
|
using Baya.Application.Contracts.Persistence;
|
||||||
|
using Baya.Infrastructure.Persistence.Interceptors;
|
||||||
using Baya.Infrastructure.Persistence.Repositories.Common;
|
using Baya.Infrastructure.Persistence.Repositories.Common;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -13,10 +14,13 @@ public static class ServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||||
|
|
||||||
services.AddDbContext<ApplicationDbContext>(options =>
|
services.AddScoped<AuditFieldInterceptor>();
|
||||||
|
|
||||||
|
services.AddDbContext<ApplicationDbContext>((serviceProvider, options) =>
|
||||||
{
|
{
|
||||||
options
|
options
|
||||||
.UseSqlServer(configuration.GetConnectionString("SqlServer"));
|
.UseSqlServer(configuration.GetConnectionString("SqlServer"))
|
||||||
|
.AddInterceptors(serviceProvider.GetRequiredService<AuditFieldInterceptor>());
|
||||||
});
|
});
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using Baya.Application.Contracts.Common;
|
||||||
|
using Baya.Domain.Common;
|
||||||
|
using Baya.Infrastructure.Persistence.Interceptors;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
|
namespace Baya.Test.Foundation;
|
||||||
|
|
||||||
|
public class AuditFieldInterceptorTests
|
||||||
|
{
|
||||||
|
// A throwaway auditable entity + context so the interceptor is tested in isolation, without the
|
||||||
|
// full Identity schema.
|
||||||
|
private sealed class Widget : BaseEntity
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class AuditTestDbContext(DbContextOptions options) : DbContext(options)
|
||||||
|
{
|
||||||
|
public DbSet<Widget> Widgets => Set<Widget>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AuditTestDbContext CreateContext(AuditFieldInterceptor interceptor)
|
||||||
|
{
|
||||||
|
var connection = new SqliteConnection("DataSource=:memory:");
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
var options = new DbContextOptionsBuilder<AuditTestDbContext>()
|
||||||
|
.UseSqlite(connection)
|
||||||
|
.AddInterceptors(interceptor)
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
var context = new AuditTestDbContext(options);
|
||||||
|
context.Database.EnsureCreated();
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SavingChanges_OnAdd_StampsCreatedAtAndCreatedById()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var fixedNow = new DateTimeOffset(2026, 6, 28, 10, 0, 0, TimeSpan.Zero);
|
||||||
|
|
||||||
|
var clock = Substitute.For<IDateTimeProvider>();
|
||||||
|
clock.UtcNow.Returns(fixedNow);
|
||||||
|
|
||||||
|
var currentUser = Substitute.For<ICurrentUser>();
|
||||||
|
currentUser.UserId.Returns(42);
|
||||||
|
|
||||||
|
await using var context = CreateContext(new AuditFieldInterceptor(currentUser, clock));
|
||||||
|
var widget = new Widget { Name = "test" };
|
||||||
|
context.Widgets.Add(widget);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(fixedNow, widget.CreatedAt);
|
||||||
|
Assert.Equal(fixedNow, widget.ModifiedAt);
|
||||||
|
Assert.Equal(42, widget.CreatedById);
|
||||||
|
Assert.Equal(42, widget.ModifiedById);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SavingChanges_OnUpdate_StampsModifiedButNotCreatedBy()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var createNow = new DateTimeOffset(2026, 6, 28, 10, 0, 0, TimeSpan.Zero);
|
||||||
|
var updateNow = new DateTimeOffset(2026, 6, 28, 12, 0, 0, TimeSpan.Zero);
|
||||||
|
|
||||||
|
var clock = Substitute.For<IDateTimeProvider>();
|
||||||
|
var currentUser = Substitute.For<ICurrentUser>();
|
||||||
|
|
||||||
|
clock.UtcNow.Returns(createNow);
|
||||||
|
currentUser.UserId.Returns(7);
|
||||||
|
|
||||||
|
await using var context = CreateContext(new AuditFieldInterceptor(currentUser, clock));
|
||||||
|
var widget = new Widget { Name = "before" };
|
||||||
|
context.Widgets.Add(widget);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Act — a different user edits the row later.
|
||||||
|
clock.UtcNow.Returns(updateNow);
|
||||||
|
currentUser.UserId.Returns(9);
|
||||||
|
widget.Name = "after";
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(createNow, widget.CreatedAt);
|
||||||
|
Assert.Equal(7, widget.CreatedById);
|
||||||
|
Assert.Equal(updateNow, widget.ModifiedAt);
|
||||||
|
Assert.Equal(9, widget.ModifiedById);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
|
<PackageReference Include="NSubstitute" />
|
||||||
|
<PackageReference Include="xunit" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="coverlet.collector">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Baya.Tests.Setup\Baya.Tests.Setup.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Infrastructure\Baya.Infrastructure.CrossCutting\Baya.Infrastructure.CrossCutting.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Baya.Application.Contracts.Common;
|
||||||
|
using Baya.Application.Features.System.Queries.Ping;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
|
namespace Baya.Test.Foundation;
|
||||||
|
|
||||||
|
public class PingQueryHandlerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ReturnsSuccess_WithServerTimeFromProvider()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var now = new DateTimeOffset(2026, 6, 28, 9, 30, 0, TimeSpan.Zero);
|
||||||
|
var clock = Substitute.For<IDateTimeProvider>();
|
||||||
|
clock.UtcNow.Returns(now);
|
||||||
|
|
||||||
|
var handler = new PingQueryHandler(clock);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await handler.Handle(new PingQuery(), CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.NotNull(result.Result);
|
||||||
|
Assert.Equal("ok", result.Result!.Status);
|
||||||
|
Assert.Equal(now, result.Result.ServerTimeUtc);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using Baya.Infrastructure.CrossCutting.Seams;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Baya.Test.Foundation;
|
||||||
|
|
||||||
|
public class SymmetricFieldEncryptorTests
|
||||||
|
{
|
||||||
|
private static SymmetricFieldEncryptor CreateEncryptor()
|
||||||
|
{
|
||||||
|
var options = Options.Create(new SeamOptions
|
||||||
|
{
|
||||||
|
FieldEncryption = new FieldEncryptionOptions
|
||||||
|
{
|
||||||
|
Key = "unit-test-field-encryption-key",
|
||||||
|
HashKey = "unit-test-hash-key"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new SymmetricFieldEncryptor(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Decrypt_OfEncrypt_ReturnsOriginalPlaintext()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var encryptor = CreateEncryptor();
|
||||||
|
const string plaintext = "09123456789";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var cipher = encryptor.Encrypt(plaintext);
|
||||||
|
var roundTripped = encryptor.Decrypt(cipher);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotEqual(plaintext, cipher);
|
||||||
|
Assert.Equal(plaintext, roundTripped);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Encrypt_SameInputTwice_ProducesDifferentCiphertext()
|
||||||
|
{
|
||||||
|
// Arrange — a random IV per call means ciphertext is not deterministic (semantic security).
|
||||||
|
var encryptor = CreateEncryptor();
|
||||||
|
const string plaintext = "IR820540102680020817909002";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var first = encryptor.Encrypt(plaintext);
|
||||||
|
var second = encryptor.Encrypt(plaintext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotEqual(first, second);
|
||||||
|
Assert.Equal(plaintext, encryptor.Decrypt(first));
|
||||||
|
Assert.Equal(plaintext, encryptor.Decrypt(second));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Hash_IsDeterministic_ForLookups()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var encryptor = CreateEncryptor();
|
||||||
|
const string value = "IR820540102680020817909002";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var first = encryptor.Hash(value);
|
||||||
|
var second = encryptor.Hash(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(first, second);
|
||||||
|
Assert.NotEqual(value, first);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
global using Xunit;
|
||||||
Reference in New Issue
Block a user