# 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:** - — automatically redirects to **** > 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 ` 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. ```