Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
b795e1e
docs: design UUIDv7 identity keys
JacobChwastek Jul 11, 2026
535aa47
feat: use Guid keys for portal identity
JacobChwastek Jul 11, 2026
e072c7a
test: cover UUIDv7 role ids
JacobChwastek Jul 11, 2026
a89a1b7
docs: plan UUIDv7 identity key migration
JacobChwastek Jul 11, 2026
4f7ee53
docs: design identity rebuild console action
JacobChwastek Jul 11, 2026
6a6b95c
feat: add identity rebuild console action
JacobChwastek Jul 11, 2026
8a4b0d0
feat(portal): forward stock market MIC
JacobChwastek Jul 12, 2026
26ae1f2
fix(portal): include local stock package in container build
JacobChwastek Jul 12, 2026
2de2a58
fix(portal): keep NuGet credentials out of image layers
JacobChwastek Jul 12, 2026
a5ca603
fix(portal): clean temporary NuGet credentials on failure
JacobChwastek Jul 12, 2026
120b3fc
fix(portal): pin Dockerfile frontend syntax
JacobChwastek Jul 12, 2026
efc31de
test(portal): cover market catalog forwarding
JacobChwastek Jul 12, 2026
49eaa0c
test(portal): remove the integration test project
JacobChwastek Jul 31, 2026
14cb6cf
build(portal): pin dotnet-ef and drop the DbUp wrapper script
JacobChwastek Jul 31, 2026
f6c995a
docs(portal): unify agent guidance into AGENTS.md
JacobChwastek Jul 31, 2026
a2c08b2
docs(portal): design library extraction and Aspire migration
JacobChwastek Aug 1, 2026
69329b1
chore(portal): move superpowers artifacts out of the repository
JacobChwastek Aug 1, 2026
be121d8
refactor(portal): extract clean-architecture libraries
JacobChwastek Aug 1, 2026
84c3fe9
refactor(portal): declare the permission port in the application layer
JacobChwastek Aug 1, 2026
750d977
refactor(portal): deliver realtime events through a port
JacobChwastek Aug 1, 2026
0c0a8c3
fix(portal): patch the high-severity Microsoft.OpenApi advisory
JacobChwastek Aug 1, 2026
da00da8
chore(portal): update NuGet dependencies
JacobChwastek Aug 1, 2026
47969ec
refactor(portal): move event handling out of the web project
JacobChwastek Aug 1, 2026
68f6f85
refactor(portal): merge the notification handlers into one
JacobChwastek Aug 1, 2026
7f4038c
build(portal): retire the Dockerfile for SDK container publish
JacobChwastek Aug 1, 2026
d2cf769
feat(portal): add readiness and liveness endpoints, and build the sol…
JacobChwastek Aug 1, 2026
563b3b8
feat(portal): model the dev stack in an Aspire AppHost
JacobChwastek Aug 1, 2026
9bfedf9
docs(portal): document the Aspire stack and the current project layout
JacobChwastek Aug 1, 2026
e93b465
feat(portal): run RabbitMQ from the Aspire AppHost with the shared de…
JacobChwastek Aug 1, 2026
112dba4
feat(portal): regenerate the EF identity baseline
JacobChwastek Aug 1, 2026
a3a54b8
fix(portal): make the AppHost usable from the IDE
JacobChwastek Aug 1, 2026
727623b
fix(portal): repair malformed development appsettings
JacobChwastek Aug 1, 2026
fe67bec
refactor(portal): retire the Compose stack in favour of the AppHost
JacobChwastek Aug 1, 2026
8e52293
feat(portal): emit traces, metrics and structured logs over OTLP
JacobChwastek Aug 1, 2026
452243d
feat(portal): record authentication outcomes as a metric
JacobChwastek Aug 1, 2026
4d300be
refactor(portal): tie container lifetime to the app and tighten comments
JacobChwastek Aug 1, 2026
a7e4a7c
chore(portal): update NuGet dependencies
JacobChwastek Aug 1, 2026
c2de517
refactor(portal): extract the Wolverine telemetry identifiers
JacobChwastek Aug 1, 2026
596736e
refactor(portal): extract Core.Abstractions as the innermost layer
JacobChwastek Aug 2, 2026
64a2ddb
refactor(portal): drop the duplicated Wolverine telemetry constants
JacobChwastek Aug 2, 2026
47846cf
style(portal): strip the AppHost narration and unpin its formatting i…
JacobChwastek Aug 2, 2026
b439039
feat(portal): share the broker with sibling stacks over wisave-net
JacobChwastek Aug 2, 2026
1fc6718
fix(portal): let aspire run start the http launch profile
JacobChwastek Aug 2, 2026
e1fa5b2
docs(portal): correct three stale claims in AGENTS.md
JacobChwastek Aug 2, 2026
bd90a37
build(portal): resolve WiSave.Stock.Contracts from the feed, not a ve…
JacobChwastek Aug 2, 2026
78ead33
chore(portal): drop the deleted docker-compose.yml from the solution
JacobChwastek Aug 2, 2026
4b7d990
fix(portal): handle unavailable downstream services
JacobChwastek Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.9",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}
5 changes: 5 additions & 0 deletions .github/workflows/portal-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,10 @@ jobs:
- name: Restore solution
run: dotnet restore WiSave.Portal.slnx --configfile NuGet.Config

# Compiles every project, not just what the test project happens to reference.
# Without this, a project outside the test graph can break without CI noticing.
- name: Build solution
run: dotnet build WiSave.Portal.slnx --configuration Release --no-restore

- name: Run portal unit tests
run: dotnet test tests/WiSave.Portal.UnitTests/WiSave.Portal.UnitTests.csproj --configuration Release --no-restore
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ graphify-out/
## OS
.DS_Store
Thumbs.db

## Superpowers artifacts (specs/plans live in the Obsidian vault)
docs/superpowers/
275 changes: 230 additions & 45 deletions AGENTS.md

Large diffs are not rendered by default.

67 changes: 3 additions & 64 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,67 +1,6 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
All guidance for this repository lives in [AGENTS.md](AGENTS.md). It is the single
source of truth for every coding agent — do not add project guidance here.

## Build & Run Commands

```bash
# Build
dotnet build

# Run the portal (requires Postgres + Redis; see docker-compose for local infra)
dotnet run --project src/WiSave.Portal

# Run all tests (uses in-memory database, no external deps needed)
dotnet test

# Run a single test by fully-qualified name
dotnet test --filter "FullyQualifiedName~WiSave.Portal.Tests.Auth.AuthEndpointsTests.Register_ReturnsOk"

# Run tests in a specific class
dotnet test --filter "FullyQualifiedName~WiSave.Portal.Tests.Auth.AuthEndpointsTests"

# Start local infrastructure only; the portal container is behind an explicit profile.
docker compose up -d

# Start local infrastructure plus the Dockerfile-built portal image
docker compose --profile portal up -d --build

# Generate a DbUp migration SQL from EF Core migrations
./scripts/generate-dbup-script.sh <from-migration|0> <to-migration> <output-sql-path>
```

## Architecture

**WiSave Portal** is an API gateway/portal service built on ASP.NET Core (.NET 10). It authenticates users, resolves plan-based permissions, and proxies requests to downstream microservices via YARP reverse proxy.

### Solution Projects

- **WiSave.Portal** — Main application: auth endpoints, YARP gateway, session management, authorization middleware
- **WiSave.Portal.Migrations** — DbUp-based PostgreSQL migrations (SQL scripts in `Scripts/`, run via `DbMigrator.Run()`)
- **WiSave.Portal.Tests** — xUnit v3 integration tests using `WebApplicationFactory` with in-memory database
- **WiSave.Portal.EfTools** — Design-time helper for EF Core CLI tooling (used by `generate-dbup-script.sh`)

### Key Layers (within WiSave.Portal)

- **Auth** (`/Auth`) — ASP.NET Core Identity configuration, `ApplicationUser` model (extends `IdentityUser` with `Name` and `PlanId`)
- **Authorization** (`/Authorization`) — `PermissionResolutionMiddleware` resolves user permissions from their plan, caches via `UserPlanCache` and `PlanPermissionCache` (1hr TTL, distributed cache)
- **Gateway** (`/Gateway`) — YARP config (`YarpConfiguration.cs`), `UserHeaderTransform` strips client identity headers and injects authenticated user info (X-User-Id, X-User-Email, X-User-Roles, X-User-Permissions)
- **Session** (`/Session`) — Redis-backed session storage (`RedisTicketStore`), falls back to in-memory cache
- **Endpoints** (`/Endpoints`) — Auth endpoint mappings at `/api/auth` (register, login, logout, me, antiforgery-token)
- **Infrastructure/Database** (`/Infrastructure/Database`) — `PortalDbContext` with Identity tables + Plans, Permissions, PlanPermissions

### Permission Model

Users belong to a **Plan** (free/standard/premium). Each plan maps to a set of **Permissions** (e.g., `incomes:read`, `stocks:write`). Admin roles (`superadmin`, `admin`) get wildcard `*` permissions. Resolved permissions are injected into proxy headers for downstream services.

### Proxy Routes

YARP routes `/api/incomes/{**remainder}` and `/api/stocks/{**remainder}` to downstream service clusters. Antiforgery validation is enforced on unsafe HTTP methods (POST/PUT/DELETE/PATCH) in the proxy pipeline.

### Local Development

Default downstream targets: incomes at `localhost:5114`, stocks at `localhost:5086`. Override via `ReverseProxy:Clusters` config or environment variables. CORS allows `http://localhost:4200` (Angular frontend). API docs available at `/scalar/v1` in Development mode.

## Testing

Tests use `WebApplicationFactory` with `UseInMemoryDatabase=true` — no external services required. Each test seeds its own roles and plans. `UserHeaderTransformTests` spins up a local echo server to validate proxy header injection.
@AGENTS.md
49 changes: 30 additions & 19 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,31 +1,42 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Aspire.Hosting.AppHost" Version="13.4.6" />
<PackageVersion Include="Aspire.Hosting.PostgreSQL" Version="13.4.6" />
<PackageVersion Include="Aspire.Hosting.RabbitMQ" Version="13.4.6" />
<PackageVersion Include="Aspire.Hosting.Redis" Version="13.4.6" />
<PackageVersion Include="dbup-postgresql" Version="7.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
<PackageVersion Include="MessagePack" Version="3.1.7" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="Scalar.AspNetCore" Version="2.16.10" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.10" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.17.0" />
<PackageVersion Include="Microsoft.OpenApi" Version="2.11.0" />
<PackageVersion Include="MessagePack" Version="3.1.8" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
<PackageVersion Include="NSubstitute" Version="6.0.0" />
<PackageVersion Include="Scalar.AspNetCore" Version="2.16.17" />
<PackageVersion Include="WiSave.Expenses.Contracts" Version="1.0.9" />
<PackageVersion Include="WiSave.Framework.Messaging.Wolverine" Version="1.0.5" />
<PackageVersion Include="WiSave.Incomes.Contracts" Version="1.0.6" />
<PackageVersion Include="WiSave.Stock.Contracts" Version="1.0.1" />
<PackageVersion Include="WolverineFx.RabbitMQ" Version="6.16.0" />
<PackageVersion Include="WolverineFx.RuntimeCompilation" Version="6.16.0" />
<PackageVersion Include="WiSave.Stock.Contracts" Version="2.0.4" />
<PackageVersion Include="WolverineFx.RabbitMQ" Version="6.24.2" />
<PackageVersion Include="WolverineFx.RuntimeCompilation" Version="6.24.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="Yarp.ReverseProxy" Version="2.3.0" />
Expand Down
10 changes: 7 additions & 3 deletions WiSave.Portal.slnx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/WiSave.Portal.AppHost/WiSave.Portal.AppHost.csproj" />
<Project Path="src/WiSave.Portal.Console/WiSave.Portal.Console.csproj" />
<Project Path="src/WiSave.Portal.Contracts/WiSave.Portal.Contracts.csproj" />
<Project Path="src/WiSave.Portal.Core.Abstractions/WiSave.Portal.Core.Abstractions.csproj" />
<Project Path="src/WiSave.Portal.Core.Application/WiSave.Portal.Core.Application.csproj" />
<Project Path="src/WiSave.Portal.Core.Infrastructure/WiSave.Portal.Core.Infrastructure.csproj" />
<Project Path="src/WiSave.Portal.Migrations/WiSave.Portal.Migrations.csproj" />
<Project Path="src/WiSave.Portal/WiSave.Portal.csproj" />
<Project Path="src/WiSave.Portal.WebApi/WiSave.Portal.WebApi.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/WiSave.Portal.UnitTests/WiSave.Portal.UnitTests.csproj" />
<Project Path="tests/WiSave.Portal.IntegrationTests/WiSave.Portal.IntegrationTests.csproj" />
</Folder>
<Folder Name="/Solution Items/">
<File Path="AGENTS.md" />
<File Path="CLAUDE.md" />
<File Path="docker-compose.yml" />
<File Path="Directory.Packages.props" />
</Folder>
</Solution>
5 changes: 5 additions & 0 deletions aspire.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"appHost": {
"path": "src/WiSave.Portal.AppHost/WiSave.Portal.AppHost.csproj"
}
}
94 changes: 0 additions & 94 deletions docker-compose.yml

This file was deleted.

Loading