Initial commit
This commit is contained in:
248
README.md
Normal file
248
README.md
Normal 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, 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
|
||||
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.
|
||||
```
|
||||
Reference in New Issue
Block a user