Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .claude/rules/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ paths:
# API and hosting

`EvilCase.Host` is the composition root. One process serves everything: `/api/**` is the API,
every other path returns the frontend's `index.html`.
`/mcp` the MCP server (SDD-023), every other path returns the frontend's `index.html`.

- Same-origin only. No CORS anywhere.
- Security headers including the CSP come from `SecurityHeadersMiddleware`. Changing an inline
Expand Down
16 changes: 8 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ Node.js 22+ is a build prerequisite: the App build runs `npm ci` and generates t
## Solution map

All code lives in `src/` (solution `EvilCase.slnx`). `EvilCase.Host` is the only runnable
project: one process serves `/api/**` and returns the frontend on every other path.
`Api/EvilCase.Api` is the API as a library, `Api.Client` the typed client generated from the
controllers, `Api.Contract` the shared DTOs. `App/EvilCase.App` is the Blazor WebAssembly
frontend. `Business/EvilCase.Business` holds the business logic, `Business/EvilCase.Domain` the
dependency-free shared kernel, `Common/EvilCase.Auth` authentication, `Common/EvilCase.Files` file
storage on disk. `Data/EvilCase.Data` is the EF Core model (PostgreSQL) and the way to the
database; `Data.Migrations` its migrations. `Tests/EvilCase.Tests` holds the application tests
(NUnit), `Utils/EvilBrains.*` the shared libraries and their tests.
project: one process serves `/api/**`, the MCP server at `/mcp` (SDD-023) and the frontend on
every other path. `Api/EvilCase.Api` is the API as a library, `Api.Client` the typed client
generated from the controllers, `Api.Contract` the shared DTOs. `App/EvilCase.App` is the Blazor
WebAssembly frontend. `Business/EvilCase.Business` holds the business logic,
`Business/EvilCase.Domain` the dependency-free shared kernel, `Common/EvilCase.Auth` authentication,
`Common/EvilCase.Files` file storage on disk. `Data/EvilCase.Data` is the EF Core model (PostgreSQL)
and the way to the database; `Data.Migrations` its migrations. `Tests/EvilCase.Tests` holds the
application tests (NUnit), `Utils/EvilBrains.*` the shared libraries and their tests.

The rules are in `.claude/rules/` and load themselves — always, or when a file in their area is
read. Detail sits next to what it describes: `docs/product/vision.md`, `docs/sdd/`,
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,13 @@ curl -H "Authorization: Bearer evc_…" "https://localhost:5000/api/cases?take=2
```

The token acts as its user, tenant included. A read-only token gets 403 on every write, and an unknown, revoked or expired token gets 401. `/api/auth/**` and token management accept the browser session only. `/openapi/v1.json` describes every endpoint and the Bearer scheme, and is anonymous in every environment.

## AI agents over MCP

`/mcp` is an MCP server (Streamable HTTP) for AI agents, with a tool for every REST resource except token management and the file archive. It accepts an API token only; the browser session and an access token get 401. Connect Claude Code:

```
claude mcp add --transport http evilcase https://<host>/mcp --header "Authorization: Bearer <token>"
```

A read-only token lists and calls only the read tools. File bytes never travel through a tool: `create_file_upload_link` and `create_file_download_link` return a one-time URL, valid 10 minutes, with a curl example; the upload is a PUT of the raw bytes. `read_file_text` returns the text of a text file, PDF or DOCX in pages of 50,000 characters.
4 changes: 4 additions & 0 deletions deploy/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ EVILCASE_TAG=latest
# forwarded headers of the reverse proxy in front, so nothing else may reach it.
EVILCASE_PORT=8080

# Public host name the reverse proxy serves, without scheme or port. The application answers to it
# alone (and to localhost for the health check), and builds the MCP file links from it.
EVILCASE_HOST=evilcase.example.com

EVILCASE_CONNECTION_STRING=Host=db.example.com;Port=5432;Database=evilcase;Username=evilcase;Password=

# Signing key for the JWT tokens, at least 32 characters. Startup fails on an empty or shorter one.
Expand Down
4 changes: 3 additions & 1 deletion deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ Two keys under `EvilBrains:EvilCase:Hosting` adapt the pipeline to what sits in
- `BehindReverseProxy` (default `false`) calls `UseForwardedHeaders` first, with `KnownIPNetworks` and `KnownProxies` cleared and `ForwardLimit = 1`. The single trusted hop is the whole defence: a deployment that turns it on must not be reachable except through that proxy.
- `HttpsRedirection` (default `true`) turns `UseHttpsRedirection` off where something in front already redirects. `/health/*` is excluded from redirection either way.

The one-time file links the MCP server hands out are absolute URLs built from the scheme and host of the `/mcp` request. `AllowedHosts` (top-level key) lists the hosts the application answers to; production sets it to the public host plus `localhost`, which the container health check calls. Behind a proxy, `BehindReverseProxy` supplies the scheme from `X-Forwarded-Proto`; the host is the `Host` header, so the proxy forwards the original one.

## Outbound network

Verifying the seal of an ISDS data message asks PostSignum for the revocation status of its
Expand All @@ -49,7 +51,7 @@ certificate: OCSP and CRL over plain HTTP at the URLs the PostSignum certificate

The database is not part of the stack — `EVILCASE_CONNECTION_STRING` points at an existing PostgreSQL, version 18 or later. The schema is migrated on startup unless `EvilBrains__EvilCase__Database__MigrateOnStartup=false` is added to the service environment; where more than one instance starts at once, roll it out separately from the idempotent `database.sql` artifact CI publishes. The role in the connection string must be able to `CREATE EXTENSION`, for `unaccent` and `pg_trgm`, and `CREATE SCHEMA`, for the `history` schema.

The service is published over plain HTTP for a reverse proxy that terminates TLS, so it sets `BehindReverseProxy=true` and `HttpsRedirection=false`; the port is published on `127.0.0.1` only (`EVILCASE_PORT` picks the host port), keeping the service unreachable except through the proxy.
The service is published over plain HTTP for a reverse proxy that terminates TLS, so it sets `BehindReverseProxy=true` and `HttpsRedirection=false`. `EVILCASE_HOST` is the public host name; the stack sets `AllowedHosts` to it and `localhost`. The port is published on `127.0.0.1` only (`EVILCASE_PORT` picks the host port), keeping the service unreachable except through the proxy.

Seq is driven by `EVILCASE_SEQ_URL` alone — an empty one logs to the console only.

Expand Down
2 changes: 2 additions & 0 deletions deploy/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ services:
# unset means no account is created, and an existing user means none is created either way.
EvilBrains__EvilCase__Auth__Seed__Email: ${EVILCASE_SEED_EMAIL:-}
EvilBrains__EvilCase__Auth__Seed__Password: ${EVILCASE_SEED_PASSWORD:-}
# The public host, which the file links of the MCP server name, and the one the health check calls.
AllowedHosts: "${EVILCASE_HOST:?};localhost"
EvilBrains__EvilCase__Hosting__BehindReverseProxy: "true"
# The proxy owns the redirect. Kestrel serves plain HTTP here, so the middleware could not
# resolve a target port anyway and would only log a warning per request.
Expand Down
4 changes: 3 additions & 1 deletion docs/sdd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ SDD (software design document) závazně popisuje jednu oblast návrhu EvilCase.
## Pořadí čtení

Architektura 001 jako první. Průřezová 002–005 a 018 platí pro každou změnu ve své oblasti;
základ 006–008 (tenance, model, číslování), agendy 009–013 a 019, aplikace 015–017, 020–022.
základ 006–008 (tenance, model, číslování), agendy 009–013 a 019, aplikace 015–017, 020–022,
MCP server 023.

## Mapa SDD ↔ milník

Expand All @@ -41,3 +42,4 @@ základ 006–008 (tenance, model, číslování), agendy 009–013 a 019, aplik
| [020](sdd-020-vzhled.md) | Vzhled | M9 |
| [021](sdd-021-vyhledavani.md) | Vyhledávání | M9 |
| [022](sdd-022-export.md) | Export | — |
| [023](sdd-023-mcp-server.md) | MCP server | průřez |
4 changes: 2 additions & 2 deletions docs/sdd/sdd-001-architektura.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ SDD-012. `.claude/rules/` nese z tohoto návrhu invarianty pro práci s kódem.

### Běhový model

Jedna aplikace, jeden proces. `EvilCase.Host` obsluhuje `/api/**` jako REST API a na každé
jiné cestě vrací `index.html` frontendu. Frontend je Blazor WebAssembly: běží v prohlížeči
Jedna aplikace, jeden proces. `EvilCase.Host` obsluhuje `/api/**` jako REST API, `/mcp` jako
MCP server pro AI agenty (SDD-023) a na každé jiné cestě vrací `index.html` frontendu. Frontend je Blazor WebAssembly: běží v prohlížeči
a se serverem mluví jen přes API, typovaným klientem (SDD-005).

Stav drží PostgreSQL; bajty souborů leží na souborovém systému, databáze nese jejich
Expand Down
7 changes: 4 additions & 3 deletions docs/sdd/sdd-002-logovani-a-observabilita.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ Co se loguje a co nikdy.
### Pipeline

Logování jde přes `EvilBrains.Logging.*` na serveru i ve WebAssembly. Seq zapíná URL
z prostředí; bez ní se loguje jen do konzole. Logují se požadavky na `/api/**` kromě úspěšného
uploadu klientských logů — ten by se sám sebou zalogoval do dalšího uploadu. Mimo `/api/**` se
z prostředí; bez ní se loguje jen do konzole. Logují se požadavky na `/api/**` a `/mcp` kromě
úspěšného uploadu klientských logů — ten by se sám sebou zalogoval do dalšího uploadu. Mimo ně se
nikde neloguje nic, health checks včetně; výjimka je výjimka nebo odpověď 5xx, ta se loguje vždy.

### Co loguje každá agenda
Expand All @@ -39,7 +39,8 @@ entit, v každé agendě stejně. Výjimkou je seed vzorových dat, který loguj

### Nikdy

Obsah souborů, těla komentářů, obsah reálných spisů. Log nese identifikátory, ne obsah.
Obsah souborů, těla komentářů, obsah reálných spisů, adresa jednorázového odkazu na soubor
(SDD-023). Log nese identifikátory, ne obsah.

## Rozhodnutí

Expand Down
17 changes: 12 additions & 5 deletions docs/sdd/sdd-005-api-konvence.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
- **Stav:** platí
- **Milníky:** průřez
- **Související SDD:** [004](sdd-004-validace-a-chyby.md), [006](sdd-006-tenance-a-ucty.md),
[016](sdd-016-navigace-a-vzhled.md)
[016](sdd-016-navigace-a-vzhled.md), [023](sdd-023-mcp-server.md)

## Rozsah

Expand All @@ -15,11 +15,12 @@ Tvary API a klienta.

- Kontrolery jsou jediný zdroj pravdy; klient se generuje z jejich zdrojů; DTOs žijí
v `EvilCase.Api.Contract`.
- Jeden proces, API pod `/api/**`, same-origin, bez CORS. Neznámá cesta pod `/api` je 404
jako Problem Details, nikdy `index.html` frontendu.
- Jeden proces, API pod `/api/**`, MCP server na `/mcp` (SDD-023), same-origin, bez CORS.
Neznámá cesta pod `/api` je 404 jako Problem Details, nikdy `index.html` frontendu.
- Business služba vrací kontraktní DTO; žádná druhá sada modelů.
- Endpoint bez výslovné výjimky vyžaduje přihlášení. Anonymní jsou přihlášení, obnova tokenu,
odhlášení, health checks, upload klientských logů, 404 pod `/api`, v každém prostředí
odhlášení, health checks, upload klientských logů, upload a download jednorázovým odkazem
`/api/file-links` (přihlášením je odkaz sám, SDD-023), 404 pod `/api`, v každém prostředí
`/openapi/v1.json` a v Development `/scalar`.
- Přihlášení nese cookie, nebo API token v hlavičce `Authorization: Bearer` (SDD-006). OpenAPI
dokument deklaruje schéma Bearer.
Expand Down Expand Up @@ -57,13 +58,19 @@ s `Retry-After`.
| Hledání | `/api/search?q=`, viz [SDD-021](sdd-021-vyhledavani.md) |
| Štítky | `/api/labels`, `/api/labels/{id}`; přidělení na vlastníku `…/labels` |
| API tokeny | `/api/api-tokens`, zrušení `/api/api-tokens/{id}`; jen přihlášení v prohlížeči (SDD-006) |
| Jednorázové odkazy | `/api/file-links?secret=`: GET stáhne obsah, PUT s `&fileName=` nahraje surové bajty; bez přihlášení (SDD-023) |
| MCP | `/mcp`, nástroje nad všemi zdroji výše kromě API tokenů a archivu souborů; jen API token (SDD-023) |

- Id v routách je `Guid`.
- Tenanta i uživatele dodává kontext požadavku; endpoint ani dotaz nikdy neberou id tenanta
parametrem (SDD-006).
- Upload a download jdou přes ručně psaný klient, ne přes generovaný: generátor neumí
multipart formulář ani proud bajtů. Upload nové verze také. Výpis, přejmenování a smazání souboru
a výpis a smazání verzí jdou přes generovaný.
a výpis a smazání verzí jdou přes generovaný. Jednorázové odkazy generovaný klient nemá; vydávají
je nástroje MCP.
- Upload jednorázovým odkazem drží pravidla uploadu (SDD-012) a odpovídá jako upload na vlastníku.
Download jednorázovým odkazem odpovídá jako download obsahu a nese `Cache-Control: no-store`.
Neznámý, použitý nebo prošlý odkaz je 404.

## Rozhodnutí

Expand Down
13 changes: 8 additions & 5 deletions docs/sdd/sdd-006-tenance-a-ucty.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
- **Milníky:** M2
- **Související SDD:** [002](sdd-002-logovani-a-observabilita.md), [003](sdd-003-testovani.md),
[004](sdd-004-validace-a-chyby.md), [005](sdd-005-api-konvence.md),
[007](sdd-007-domenovy-model.md), [019](sdd-019-stitky.md)
[007](sdd-007-domenovy-model.md), [019](sdd-019-stitky.md), [023](sdd-023-mcp-server.md)

## Rozsah

Expand All @@ -20,7 +20,8 @@ popisuje SDD-007.
- **User** — tenantová entita, patří právě jednomu tenantu: e-mail, hash hesla, role, lockout
a `TenantId`.

Mimo tenant stojí jen Account, Tenant, refresh token a API token; každá jiná entita nese
Mimo tenant stojí jen Account, Tenant, refresh token, API token a jednorázový odkaz na soubor
(SDD-023); každá jiná entita nese
`TenantId`.
Vlastníka `UserId` nese každá kromě kontaktu; kontakt patří tenantu (SDD-011). Obojí plní
zápis, ne volající. Tenanta ani uživatele nelze smazat, dokud drží řádky.
Expand All @@ -39,7 +40,7 @@ Data nesmí utéct mezi tenanty; únik je kritická chyba.

- Čtení nikdy nevidí řádek cizího tenantu; tenanta i uživatele dodává kontext požadavku.
Výjimka je hledání uživatele, které tenanta ještě znát nemůže — přihlášení, obnova tokenu,
ověření API tokenu a oba seedy.
ověření API tokenu, použití jednorázového odkazu a oba seedy.
- Access token nese tenant claim i subject claim.
- Zápis doplní `TenantId` a `UserId` nové entity z kontextu a odmítne řádek, který jmenuje
cizího tenanta nebo cizího uživatele.
Expand All @@ -49,7 +50,7 @@ Data nesmí utéct mezi tenanty; únik je kritická chyba.
přes celé nasazení a ukládá se oříznutý a malými písmeny, takže přihlášení nerozlišuje
velikost písmen.
- Konvenční test hlídá, že žádná tenantová entita filtr nepostrádá a že mimo tenant stojí jen
Account, Tenant, refresh token a API token (SDD-003).
Account, Tenant, refresh token, API token a jednorázový odkaz na soubor (SDD-003).

### Autentizace

Expand Down Expand Up @@ -81,7 +82,9 @@ jeho hash. Posílá se v hlavičce `Authorization: Bearer` a schéma vybere jeho
v hlavičce vyhraje nad cookie přihlášení. Neznámý, zrušený nebo prošlý token odpoví 401. Zrušení
token smaže a platí od dalšího požadavku, nic token necachuje.

- Token jen pro čtení odpoví 403 na každý zápis: POST, PUT, PATCH, DELETE.
- Token jen pro čtení odpoví 403 na každý zápis: POST, PUT, PATCH, DELETE. Na `/mcp` vidí
a volá jen nástroje pro čtení (SDD-023).
- `/mcp` přijme jen API token; cookie ani access token tam neplatí (SDD-023).
- Na `/api/auth/**` a na správu tokenů odpoví token 403; obojí zůstává přihlášení v prohlížeči.
- Uzamčení účtu token nezastaví, stejně jako živé přihlášení.
- Požadavky s tokenem limit požadavků nemají (SDD-005).
Expand Down
3 changes: 2 additions & 1 deletion docs/sdd/sdd-018-prace-s-databazi.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ a refresh tokeny filtr nemají a jmenují řádku jejím id.
- Každý zápis do historizované tabulky — `Accounts`, `Tenants`, `Users`, `Contacts`, `Cases`,
`Acts`, `FileAssets`, `FileVersions`, `Comments` — zůstává navždy jako řádek se svým
operátorem a časem, i po smazání původního řádku. `RefreshTokens` historii nemá: rotují při
každém refreshi a nesou hash tokenu.
každém refreshi a nesou hash tokenu. `FileLinks` také ne: žijí deset minut a nesou hash
odkazu (SDD-023).
- Historický řádek je jen k čtení, žádný zápis ho nezmění ani nesmaže.
- Operátor je ten, kdo byl přihlášený v okamžiku zápisu, ne vlastník řádku.

Expand Down
Loading
Loading