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
/apito the API container. - The API validates JWTs, enforces login rate limiting, and talks to PostgreSQL via EF Core (parameterized queries only).
Prerequisites
- Docker and Docker Compose
- Network access from the API container to your external PostgreSQL server
- For local development without Docker: .NET 8 SDK and Node.js 20+
Production deployment (Linux server)
See docs/DEPLOYMENT.md for a full guide (Docker, firewall, PostgreSQL access, Let’s Encrypt HTTPS).
Quick version:
cp .env.example .env && nano .env
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
Quick start (Docker)
-
Copy the environment template and fill in your values:
cp .env.example .env # then edit .envRequired variables (see
.env.examplefor descriptions):Variable Description DB_CONNECTION_STRINGNpgsql connection string to the existing PostgreSQL database JWT_SECRETSigning key for JWTs (≥ 32 chars; openssl rand -base64 48)JWT_ISSUERJWT issuer identifier (e.g. DailyMeals)JWT_AUDIENCEJWT audience identifier (e.g. DailyMeals)CORS_ALLOWED_ORIGINPublic origin of the frontend (e.g. https://localhost) -
Start the stack:
docker compose up -d --build -
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
-
Open
DailyMeals.slnin Rider (open the solution root, not themeal-plan-frontendfolder alone). -
Copy local API settings (connection string is not committed):
cp MealPlan.Api/appsettings.Local.json.example MealPlan.Api/appsettings.Local.json # Edit appsettings.Local.json with your PostgreSQL connection string -
Install Node + frontend dependencies (no Homebrew
npmrequired):cd DailyMeals # repository root (where DailyMeals.sln lives) ./scripts/install-node.shAngular frontend (recommended rewrite):
cd meal-plan-frontend-angular npm install npm start # http://localhost:4200, proxies /api → localhost:5000Or React frontend:
cd meal-plan-frontend ../scripts/npm install ../scripts/npm run dev # http://localhost:5173 -
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. - DailyMeals (API + Frontend) — API (
-
Node is provided by
.toolchain/(same approach as FA_WEB). Rider run configs point at that interpreter automatically.
Local development (without Docker)
Backend:
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:
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
InMemoryRefreshTokenStorefor a shared store such as Redis.) - Rate limiting:
POST /api/auth/loginis 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:
- One page per recipe — name, category badge, calories + macros, prep time, ingredients and numbered steps.
- 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:
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-schemeby default and can be toggled manually; the preference is kept in memory only (nolocalStorage), 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.