Files
DailyMeals/README.md
Piotr Kus f15bb7a916 * Extended functionalities
* Added different UIs
2026-06-24 21:43:40 +02:00

276 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```
**Angular frontend (recommended rewrite):**
```bash
cd meal-plan-frontend-angular
npm install
npm start # http://localhost:4200, proxies /api → localhost:5000
```
**Or React frontend:**
```bash
cd meal-plan-frontend
../scripts/npm install
../scripts/npm run dev # http://localhost:5173
```
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=`, `?search=`, `?ingredient=` |
| GET | `/api/recipes/{id}` | Full recipe detail (ingredients + steps) |
| GET | `/api/recipes/categories` | Categories with active recipe counts |
| POST | `/api/recipes` | Create a recipe (ingredients + steps) |
| PUT | `/api/recipes/{id}` | Update a recipe |
| GET | `/api/recipes/import/template` | Download Excel import template (.xlsx) |
| POST | `/api/recipes/import/excel` | Import recipes from uploaded .xlsx file |
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 (original)
│ ├── src/ api, components, pages, hooks, store, types, utils
│ ├── nginx.conf TLS, gzip, security headers, /api proxy, SPA fallback
│ └── Dockerfile
├── meal-plan-frontend-angular/ Angular 19 SPA (rewrite of React frontend)
│ ├── src/app/ core, layout, features, shared
│ ├── public/i18n/ EN + PL translations
│ ├── nginx.conf
│ └── Dockerfile
├── meal-plan-frontend-mobile/ Expo (React Native) iOS + Android app
│ ├── app/ Expo Router screens
│ ├── src/ api, context, i18n, theme, components
│ └── README.md Run with `npm start` (see mobile README)
├── 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.
```