276 lines
11 KiB
Markdown
276 lines
11 KiB
Markdown
# 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, Let’s 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.
|
||
```
|