diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f90451e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,348 @@ +# AGENTS.md + +Tento repozitář obsahuje aplikaci běžící v AppFactory. + +Tento soubor je určený pro AI asistenty, vývojáře a automatizované nástroje, které budou aplikaci upravovat. + +## Kontext AppFactory + +Aplikace běží jako Docker container spravovaný AppFactory. + +AppFactory zajišťuje: + +- vytvoření Gitea repozitáře +- webhook z Gitea do AppFactory +- build Docker image +- deploy containeru +- reverse proxy přes Caddy +- runtime variables a secrets přes environment variables +- monitoring přes health endpoint + +Aplikační repozitář nemá měnit infrastrukturu AppFactory. + +## Veřejná URL a reverse proxy + +Aplikace neběží v rootu domény. + +Veřejná URL aplikace má tvar: + +```text +https://services.csbot.cz/apps/ +``` + +Příklady: + +```text +https://services.csbot.cz/apps/test-dotnet-api +https://services.csbot.cz/apps/microsoft-365-service +``` + +Aplikace musí počítat s tím, že běží za reverse proxy. + +Nikdy nehardcoduj veřejnou doménu. + +Nikdy nehardcoduj `/apps/` do business logiky, pokud framework nabízí lepší mechanismus, například: + +- `ROOT_PATH` +- `PathBase` +- `basePath` +- OpenAPI `servers` +- Swagger route prefix +- framework-specific proxy/base URL nastavení + +## Caddy routing + +AppFactory Caddy routuje aplikace přes: + +```text +/apps/ +``` + +Typicky platí: + +```text +veřejný request: +GET /apps//contacts + +aplikace uvnitř containeru často vidí: +GET /contacts +``` + +Důvodem je použití reverse proxy route typu `handle_path`, která prefix `/apps/` odstraní před předáním do containeru. + +Aplikace proto musí být napsaná tak, aby: + +- správně obsloužila interní routy +- správně generovala dokumentaci pro veřejnou proxy cestu +- Swagger UI testování používalo veřejnou cestu s `/apps/` + +## Povinné endpointy + +Každá služba musí poskytovat: + +```text +GET /health +GET /docs +``` + +Z veřejné URL musí být dostupné jako: + +```text +GET /apps//health +GET /apps//docs +``` + +`/health` musí vracet HTTP 200, pokud je aplikace schopná přijímat provoz. + +`/docs` musí poskytovat Swagger/OpenAPI dokumentaci nebo obdobnou interaktivní dokumentaci API. + +Pokud služba není HTTP API, musí i tak poskytovat minimální HTTP health endpoint. + +## Swagger / OpenAPI pravidla + +Swagger UI a OpenAPI definice musí respektovat AppFactory reverse proxy prefix. + +Pokud aplikace běží veřejně na: + +```text +https://services.csbot.cz/apps/ +``` + +pak Swagger UI musí při testování endpointů volat stejný prefix. + +Špatně: + +```text +GET https://services.csbot.cz/contacts +``` + +Správně: + +```text +GET https://services.csbot.cz/apps//contacts +``` + +Po každé úpravě API je povinné ověřit: + +1. `/health` funguje +2. `/docs` funguje +3. Swagger UI se načte +4. Swagger UI `Try it out` volá endpointy přes `/apps/` +5. OpenAPI JSON obsahuje správný server/base path +6. Nově přidané endpointy jsou ve Swagger dokumentaci + +Pokud framework generuje OpenAPI `servers`, musí obsahovat proxy prefix. + +Příklad: + +```json +{ + "servers": [ + { + "url": "/apps/" + } + ] +} +``` + +Nesmí vzniknout stav, kdy Swagger UI vypadá správně, ale tlačítko `Try it out` volá endpointy bez `/apps/`. + +## ROOT_PATH / PathBase + +AppFactory může aplikaci předávat environment variable: + +```text +ROOT_PATH=/apps/ +``` + +Použití závisí na frameworku. + +### .NET + +V ASP.NET Core použij `UsePathBase`, pokud template nebo aplikace používá `ROOT_PATH`. + +Typicky: + +```csharp +var rootPath = Environment.GetEnvironmentVariable("ROOT_PATH"); + +if (!string.IsNullOrWhiteSpace(rootPath)) +{ + app.UsePathBase(rootPath); +} +``` + +Swagger/OpenAPI ale musí být nakonfigurovaný tak, aby `servers` odpovídaly proxy prefixu. + +Nestačí pouze přidat endpoint `/docs`. + +Je nutné ověřit i Swagger `Try it out`. + +### Python / FastAPI + +FastAPI typicky používá `root_path`. + +Aplikace musí zajistit, že dokumentace a OpenAPI schema respektují proxy prefix. + +### Node.js / Express + +Express aplikace musí počítat s reverse proxy prefixem. + +Pokud se používá Swagger UI, musí být OpenAPI `servers` nebo Swagger konfigurace nastavené tak, aby testovací requesty šly přes `/apps/`. + +## Variables a secrets + +AppFactory spravuje variables a secrets přes portál. + +Portál ukládá hodnoty do DB a generuje runtime `.env` soubor aplikace. + +Aplikace je čte jako environment variables. + +Příklady: + +### .NET + +```csharp +var value = Environment.GetEnvironmentVariable("MY_VARIABLE"); +var optionalValue = builder.Configuration["OPTIONAL_VARIABLE"] ?? "default"; +``` + +### Python + +```python +import os + +value = os.getenv("MY_VARIABLE") +``` + +### Node.js + +```js +const value = process.env.MY_VARIABLE; +``` + +Secrets se nikdy nesmí: + +- commitovat do repository +- zapisovat do README +- vypisovat do logu +- vracet z běžných endpointů +- zobrazovat ve Swagger příkladech +- ukládat do zdrojového kódu +- hardcodovat + +Testovací endpointy, které vrací variables nebo secrets, se smí používat pouze dočasně pro ověření a musí být odstraněny před produkčním použitím. + +## Docker a port + +Aplikace musí poslouchat na portu definovaném AppFactory šablonou nebo metadaty aplikace. + +Port neměň bez odpovídající úpravy AppFactory konfigurace. + +Aplikace musí poslouchat na všech rozhraních containeru: + +```text +0.0.0.0 +``` + +Ne pouze na: + +```text +localhost +``` + +Dockerfile musí být deterministický a nesmí vyžadovat ruční zásahy v containeru. + +Ruční změny provedené přímo v běžícím containeru nejsou trvalé. + +## Co AI nesmí měnit v aplikačním repozitáři + +AI nesmí z aplikačního repozitáře měnit: + +- AppFactory deploy mechanismus +- Caddy konfiguraci +- Gitea webhooky +- Registry konfiguraci +- Backup/restore skripty +- Secrets storage +- Systémové soubory serveru +- AppFactory core služby +- AppFactory tools skripty + +Pokud je potřeba změnit infrastrukturu, musí se to řešit v příslušném AppFactory repozitáři, ne v repozitáři konkrétní aplikace. + +## Pravidla pro úpravy aplikace + +Před úpravou si vždy přečti: + +- `README.md` +- `AGENTS.md` +- `Dockerfile` +- hlavní vstupní soubor aplikace +- existující konfiguraci Swagger/OpenAPI +- způsob práce s environment variables + +Po úpravě ověř minimálně: + +- `/health` +- `/docs` +- upravovaný endpoint +- Swagger UI +- Swagger `Try it out` přes `/apps/` +- že container stále startuje +- že se nezměnil port bez úpravy metadat +- že secrets nejsou v logu ani ve zdrojovém kódu +- ideálně průběžně generuj dokumentaci .md do složky documentation v hlavním adresáři projektu. + +Každá změna musí zachovat kompatibilitu s AppFactory reverse proxy. + +Pokud přidáváš nový endpoint, dokumentace se musí aktualizovat současně. + +Pokud upravuješ request/response modely, Swagger/OpenAPI musí odpovídat skutečnému chování aplikace. + +## Zakázané zkratky + +Nedělej tyto věci: + +- nepřidávej endpoint, který funguje jen lokálně, ale ne přes `/apps/` +- neopravuj Swagger tak, že bude fungovat pouze na root doméně +- nevypínej Swagger kvůli proxy problému +- nevypínej health check +- nevypínej validaci secrets tím, že je začneš logovat +- nepřepisuj Dockerfile na jiný port bez úpravy AppFactory metadat +- nepřidávej hardcoded URL produkční domény do business logiky + +## Doporučený postup po změně + +Po změně aplikace ověř veřejně: + +```text +GET https://services.csbot.cz/apps//health +GET https://services.csbot.cz/apps//docs +``` + +A přes Swagger UI ověř, že testování endpointů volá URL ve tvaru: + +```text +https://services.csbot.cz/apps// +``` + +ne: + +```text +https://services.csbot.cz/ +``` + +## Shrnutí pro AI + +Nejdůležitější pravidla: + +- aplikace běží za `/apps/` +- `/health` je povinný +- `/docs` se Swaggerem je povinný +- Swagger `Try it out` musí používat `/apps/` +- OpenAPI musí mít správný base path/server +- secrets nikdy nelogovat ani necommitovat +- konfiguraci číst z environment variables +- neměnit AppFactory infrastrukturu z aplikačního repozitáře +- po každé úpravě ověř reverse proxy chování diff --git a/Program.cs b/Program.cs index 9b2c969..431b492 100644 --- a/Program.cs +++ b/Program.cs @@ -79,26 +79,33 @@ builder.Services.AddSwaggerGen(options => var app = builder.Build(); -if (!string.IsNullOrWhiteSpace(settings.RootPath)) +// ROOT_PATH carries the public reverse-proxy prefix (e.g. /apps/idoklad). AppFactory's Caddy uses +// handle_path, which strips that prefix before the request reaches the container, so PathBase ends +// up empty and the OpenAPI server URL must come from ROOT_PATH directly (see the filter below). +var publicPrefix = string.IsNullOrWhiteSpace(settings.RootPath) ? null : "/" + settings.RootPath.Trim('/'); + +// UsePathBase is a no-op under handle_path (the request no longer carries the prefix) but keeps the +// app correct behind a proxy that forwards the prefix intact, so routes still resolve in that case. +if (publicPrefix is not null) { - // UsePathBase requires a leading slash; tolerate ROOT_PATH configured without one. - var basePath = settings.RootPath.StartsWith('/') ? settings.RootPath : "/" + settings.RootPath; - app.UsePathBase(basePath); + app.UsePathBase(publicPrefix); } app.UseMiddleware(); // Serve the OpenAPI document under the same /docs prefix as the UI so a relative endpoint -// resolves correctly both locally and behind a reverse-proxy path base (ROOT_PATH). +// resolves correctly both locally and behind a reverse-proxy prefix (ROOT_PATH). app.UseSwagger(options => { options.RouteTemplate = "docs/{documentName}/swagger.json"; - // Advertise the request path base (e.g. /apps/idoklad behind the portal proxy) as the - // OpenAPI server so Swagger UI "Try it out" targets {pathBase}/contacts, not the host root. + // Advertise the public prefix (e.g. /apps/idoklad behind the portal proxy) as the OpenAPI + // server so Swagger UI "Try it out" targets {prefix}/contacts, not the host root. The prefix + // comes from ROOT_PATH because handle_path has already stripped it from the request, leaving + // httpReq.PathBase empty. Fall back to PathBase (a proxy that keeps the prefix) and finally "/". options.PreSerializeFilters.Add((swaggerDoc, httpReq) => { - var basePath = httpReq.PathBase.HasValue ? httpReq.PathBase.Value : "/"; - swaggerDoc.Servers = new List { new() { Url = basePath } }; + var serverUrl = publicPrefix ?? (httpReq.PathBase.HasValue ? httpReq.PathBase.Value : "/"); + swaggerDoc.Servers = new List { new() { Url = serverUrl } }; }); }); app.UseSwaggerUI(options => diff --git a/documentation/reverse-proxy-swagger.md b/documentation/reverse-proxy-swagger.md new file mode 100644 index 0000000..dc7825e --- /dev/null +++ b/documentation/reverse-proxy-swagger.md @@ -0,0 +1,49 @@ +# Reverse proxy a Swagger (ROOT_PATH) + +Tato služba běží v AppFactory za reverzní proxy (Caddy) na veřejné cestě: + +```text +https://services.csbot.cz/apps/idoklad +``` + +## Jak Caddy předává requesty + +Caddy používá `handle_path`, který prefix `/apps/idoklad` **odstraní** dříve, než +request dorazí do containeru. Container tedy přijímá routy bez prefixu: + +```text +veřejně: GET /apps/idoklad/contacts +container: GET /contacts +``` + +Důsledek: `HttpRequest.PathBase` je uvnitř containeru **prázdný**, protože příchozí +cesta už prefix neobsahuje. Nelze z něj proto odvodit veřejnou cestu pro OpenAPI. + +## Konfigurace v `Program.cs` + +- `ROOT_PATH` (env, např. `/apps/idoklad`) je jediný spolehlivý zdroj veřejného prefixu. +- Z `ROOT_PATH` se sestaví `publicPrefix` (normalizovaný, s úvodním lomítkem). +- `UsePathBase(publicPrefix)` je za `handle_path` no-op, ale ponechán pro případ proxy, + která prefix nestrhává (`handle`), aby routy stále seděly. +- OpenAPI `servers[0].url` se v `PreSerializeFilters` nastaví na `publicPrefix` + (fallback na `PathBase`, nakonec `/` pro lokální běh). + +Díky tomu Swagger UI „Try it out" volá `…/apps/idoklad/`, ne kořen domény. + +## Ověření + +| Kontrola | Lokálně (bez ROOT_PATH) | S `ROOT_PATH=/apps/idoklad` | +|---|---|---| +| `GET /health` | 200 | 200 | +| `GET /docs` (Swagger UI) | 200 | 200 | +| `GET /docs/v1/swagger.json` | 200 | 200 | +| `servers[0].url` v OpenAPI | `/` | `/apps/idoklad` | + +Veřejně po deploy ověř: + +```text +GET https://services.csbot.cz/apps/idoklad/health +GET https://services.csbot.cz/apps/idoklad/docs +``` + +a ve Swagger UI, že „Try it out" cílí na `https://services.csbot.cz/apps/idoklad/`.