Initial commit

This commit is contained in:
2026-06-04 06:24:56 +02:00
commit 282f4864c4
111 changed files with 8083 additions and 0 deletions

248
README.md Normal file
View File

@@ -0,0 +1,248 @@
# DailyMeals
A full-stack meal-plan web application. Browse recipes by meal category, view full
recipes with macros, ingredients and preparation steps, and export a selection of
recipes to a PDF that includes an aggregated shopping list.
- **Frontend:** React 18 + TypeScript, Vite, React Router v6, Tailwind CSS v3,
TanStack Query v5, React Hook Form + Zod, jsPDF + html2canvas, Lucide icons, Axios.
- **Backend:** ASP.NET Core 8 Web API, EF Core 8 (Npgsql), JWT auth with refresh-token
rotation, rate limiting, Serilog, full security hardening.
- **Database:** an **existing, externally hosted PostgreSQL 16** database. The app only
connects to it — it never creates, seeds, or migrates the schema.
- **Infrastructure:** Docker Compose runs the API and the frontend only (no database
container). nginx serves the SPA, terminates TLS, and reverse-proxies `/api`.
---
## Architecture
```
Browser ──HTTPS──> nginx (frontend container) ──> static SPA
└── /api ──HTTP──> ASP.NET Core API (api container) ──> external PostgreSQL
```
- nginx redirects HTTP → HTTPS, sets security headers, gzips responses and proxies
`/api` to the API container.
- The API validates JWTs, enforces login rate limiting, and talks to PostgreSQL via
EF Core (parameterized queries only).
---
## Prerequisites
- [Docker](https://docs.docker.com/get-docker/) and Docker Compose
- Network access from the API container to your external PostgreSQL server
- For local development without Docker: [.NET 8 SDK](https://dotnet.microsoft.com/) and
[Node.js 20+](https://nodejs.org/)
---
## Production deployment (Linux server)
See **[docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)** for a full guide (Docker, firewall, PostgreSQL access, Lets Encrypt HTTPS).
Quick version:
```bash
cp .env.example .env && nano .env
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
```
---
## Quick start (Docker)
1. **Copy the environment template and fill in your values:**
```bash
cp .env.example .env
# then edit .env
```
Required variables (see `.env.example` for descriptions):
| Variable | Description |
| ---------------------- | ------------------------------------------------------------------ |
| `DB_CONNECTION_STRING` | Npgsql connection string to the existing PostgreSQL database |
| `JWT_SECRET` | Signing key for JWTs (≥ 32 chars; `openssl rand -base64 48`) |
| `JWT_ISSUER` | JWT issuer identifier (e.g. `DailyMeals`) |
| `JWT_AUDIENCE` | JWT audience identifier (e.g. `DailyMeals`) |
| `CORS_ALLOWED_ORIGIN` | Public origin of the frontend (e.g. `https://localhost`) |
2. **Start the stack:**
```bash
docker compose up -d --build
```
3. **Open the application:**
- <http://localhost> — automatically redirects to **<https://localhost>**
> The frontend container ships with a **self-signed** TLS certificate so HTTPS works
> out of the box, so your browser will warn about the certificate on first visit.
> For production, replace it with a real certificate (see below).
Sign in with a user that already exists in the `AspNetUsers` table of your database.
Passwords are verified with ASP.NET Core Identity's password hasher, so the app is
compatible with hashes produced by ASP.NET Core Identity.
---
## JetBrains Rider
1. Open **`DailyMeals.sln`** in Rider (open the **solution root**, not the `meal-plan-frontend` folder alone).
2. Copy local API settings (connection string is not committed):
```bash
cp MealPlan.Api/appsettings.Local.json.example MealPlan.Api/appsettings.Local.json
# Edit appsettings.Local.json with your PostgreSQL connection string
```
3. Install Node + frontend dependencies (no Homebrew `npm` required):
```bash
cd DailyMeals # repository root (where DailyMeals.sln lives)
./scripts/install-node.sh
cd meal-plan-frontend
../scripts/npm install
```
4. In Rider, open the run configuration dropdown (top toolbar). You should see:
- **DailyMeals (API + Frontend)** — API (`http://localhost:5000`) + Vite (`http://localhost:5173`) (default).
- **MealPlan.Api (http)** — API only (Swagger at `/swagger`).
- **meal-plan-frontend (vite dev)** — frontend only.
If configs are missing: **File → Reload All from Disk**, or close Rider and reopen `DailyMeals.sln`.
5. Node is provided by **`.toolchain/`** (same approach as FA_WEB). Rider run configs point at that interpreter automatically.
---
## Local development (without Docker)
**Backend:**
```bash
cd MealPlan.Api
export ConnectionStrings__DefaultConnection="Host=...;Port=5432;Database=...;Username=...;Password=..."
export Jwt__Secret="dev-secret-at-least-32-characters-long-xxxxx"
dotnet run
# API on http://localhost:5000 (Swagger UI at /swagger in Development)
```
**Frontend:**
```bash
cd meal-plan-frontend
npm install
npm run dev
# App on http://localhost:5173 — /api is proxied to http://localhost:5000
```
---
## API reference
All `/api/recipes` endpoints require a valid `Authorization: Bearer <accessToken>` header.
| Method | Endpoint | Description |
| ------ | ------------------------- | ----------------------------------------------------- |
| POST | `/api/auth/register` | Create account → access + refresh token (rate-limited) |
| POST | `/api/auth/login` | Login → access + refresh token (rate-limited 5/min/IP)|
| POST | `/api/auth/refresh` | Rotate refresh token → new token pair |
| POST | `/api/auth/logout` | Revoke a refresh token |
| GET | `/api/auth/me` | Current user info |
| GET | `/api/recipes` | List recipes — `?category=0..3` and `?search=` filters|
| GET | `/api/recipes/{id}` | Full recipe detail (ingredients + steps) |
| GET | `/api/recipes/categories` | Categories with active recipe counts |
Meal categories: `0 = Breakfast`, `1 = SecondBreakfast`, `2 = Lunch`, `3 = Dinner`.
---
## Security
- **JWT** access tokens expire after **15 minutes**; refresh tokens after **7 days**.
- **Refresh-token rotation:** every refresh invalidates the presented token and issues a
new one. (Refresh tokens are tracked in memory to honor the "do not modify the database"
constraint — for multi-instance deployments, swap `InMemoryRefreshTokenStore` for a
shared store such as Redis.)
- **Rate limiting:** `POST /api/auth/login` is limited to 5 attempts per minute per IP.
- **CORS** is restricted to `CORS__AllowedOrigin`.
- **Security headers** are set both in the API and at the nginx edge:
`X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`,
`Referrer-Policy: no-referrer`, `Content-Security-Policy`, and HSTS.
- **HTTPS** is enforced: nginx redirects all HTTP traffic to HTTPS.
- **No raw SQL** — all data access uses EF Core parameterized queries.
- **All secrets** are provided via environment variables; nothing is hardcoded.
- **Structured logging** with Serilog to console and rolling daily files
(`MealPlan.Api/logs/`).
---
## PDF export
On the **All Meals** page, select one or more recipes and click **Generate PDF**. The PDF
contains:
1. **One page per recipe** — name, category badge, calories + macros, prep time,
ingredients and numbered steps.
2. **A final shopping-list page** — ingredients aggregated across all selected recipes:
- gram amounts of the same ingredient are summed into one line;
- non-gram units (pcs, tbsp, …) are summed separately per unit;
- "to taste" / "for serving" / "optional" items are listed once without a quantity;
- a parenthetical breakdown shows the per-recipe contributions when an ingredient
appears in more than one recipe;
- a footer lists every recipe included in the export with its category and calories.
---
## Using a real TLS certificate (production)
The frontend image generates a self-signed certificate at build time. To use a real
certificate (e.g. from Let's Encrypt), mount your cert/key over the defaults in
`docker-compose.yml`:
```yaml
frontend:
volumes:
- /path/to/fullchain.pem:/etc/nginx/certs/server.crt:ro
- /path/to/privkey.pem:/etc/nginx/certs/server.key:ro
```
---
## Project layout
```
DailyMeals/
├── MealPlan.Api/ ASP.NET Core 8 Web API
│ ├── Controllers/ AuthController, RecipesController
│ ├── Data/ AppDbContext (database-first, no migrations)
│ ├── Models/ Recipe, Ingredient, RecipeStep, ApplicationUser, ...
│ ├── DTOs/ Auth + Recipe DTOs
│ ├── Services/ AuthService, RecipeService, TokenService, token store
│ ├── Middleware/ ExceptionMiddleware
│ ├── Program.cs Composition root / pipeline
│ └── Dockerfile
├── meal-plan-frontend/ React + TypeScript SPA
│ ├── src/ api, components, pages, hooks, store, types, utils
│ ├── nginx.conf TLS, gzip, security headers, /api proxy, SPA fallback
│ └── Dockerfile
├── docker-compose.yml API + frontend (no database container)
└── .env.example
```
---
## Notes
- Designed for **Linux hosting** — no Windows-specific APIs or paths.
- **Dark mode** follows `prefers-color-scheme` by default and can be toggled manually;
the preference is kept in memory only (no `localStorage`), so it resets on reload.
- Because auth tokens are held in memory only, a full page reload returns you to the
login screen — this is intentional given the in-memory storage requirement.
```