Culinary Otter is a server-rendered recipe helper that lets users discover dishes, favorite them, build custom recipes with structured ingredients, and estimate prices via a Kroger product lookup — all backed by MySQL with session-based auth and bcrypt-hashed passwords.
- Discover: Random and detailed recipes powered by Spoonacular.
- Favorite: Save/remove favorites tied to your account.
- Customize: Create personal recipes (ingredients, instructions, dietary flags, image URL).
- Price check: Cost lookups using Kroger’s OAuth + product APIs (default
filter.locationId=01400943). - Accounts & roles: Registration, login, and role-based pages for
userandadmin. - Daily reset: The hosted site runs a daily cron that resets the database to a clean demo state.
Live demo: https://culinary-otter.r-siddiq.tech Additional context: https://www.rsiddiq.com/internet-programming.html
- Runtime: Node.js 18+
- Web: Express 5, EJS templates, Bootstrap 5
- DB: MySQL 8 (mysql2/promise pool)
- Auth: express-session (cookie-based sessions), bcrypt password hashing
- APIs: Spoonacular (recipes), Kroger (OAuth2 + product compact search)
- Dotenv: environment-based configuration
- Node.js 18+ and npm
- MySQL 8+ with a database you can import into
- Shell with
mysqlclient available (macOS/Linux or Git Bash/WSL on Windows) - API credentials: Spoonacular, Kroger
git clone <this-repo-url>
cd culinary-otter
npm installCopy the example and add API keys + a strong SESSION_SECRET:
cp .env.example .env
# then edit .envRequired keys:
DB_HOST— MySQL hostnameDB_PORT— MySQL port (default 3306)DB_USER— MySQL usernameDB_PASSWORD— MySQL passwordDB_NAME— MySQL database nameDB_CONNECTION_LIMIT— MySQL pool size (e.g. 10)SPOON_API_KEY— Spoonacular API keyKROGER_CLIENT_ID— Kroger API OAuth client idKROGER_CLIENT_SECRET— Kroger API OAuth client secretSESSION_SECRET— Random string for signing sessions
For local DB imports, copy
.my.cnf.exampleto.my.cnfin the project root and fill in your MySQL credentials. The restore script will read from that file.
Option A — scripted (recommended):
bash ./restore_db.shThis drops existing tables (safely), imports CulinaryOtter_DB.sql, and seeds demo users/data.
Option B — manual:
mysql -u <USER> -p -h <HOST> -P <PORT> <DB_NAME> < CulinaryOtter_DB.sqlnode index.mjs
# App listens on http://localhost:3000Optionally add
"start": "node index.mjs"topackage.jsonand runnpm start.
Seeded demo accounts (local only):
- admin / admin → Admin console
- user / user → Standard user
The production demo resets daily via cron, so any changes there are ephemeral by design.
Tables (InnoDB, UTF-8):
- auth_users — users with
role(admin|user),username,password(bcrypt), profile fields. - favorite_recipes — user favorites keyed by Spoonacular
recipeId. - custom_recipes — user-authored recipes with
ingredients(JSON), dietary flags, and free-form notes.
All schema & seeds live in CulinaryOtter_DB.sql. Use restore_db.sh to reset/import.
├── index.mjs # Express app (routes, auth, session, API calls)
├── CulinaryOtter_DB.sql # MySQL schema + seed data
├── restore_db.sh # Reset/import script (uses .my.cnf)
├── generate.js # Utility to generate bcrypt hashes for seeds
├── views/ # EJS templates (SSR)
│ ├── partials/{header,nav,footer}.ejs
│ └── *.ejs # Pages: login, register, index, favorites, custom recipes, admin, etc.
├── public/
│ ├── css/styles.css
│ ├── js/scripts.js
│ └── img/*.png|svg
├── .env.example # DB defaults (copy to .env and add API keys + SESSION_SECRET)
├── .my.cnf.example # Local MySQL client config for restore_db.sh
├── package.json
└── package-lock.json
GET /— HomeGET /index— Landing feedGET /about,GET /contact,GET /terms,GET /privacy— Static pages
GET /login— Login pagePOST /login— Body:username,passwordGET /register— Registration pagePOST /register— Body:username,firstName,lastName,password,confirmPasswordGET /logout— Destroy session- Portals:
GET /welcomeUser,GET /welcomeAdmin
- Browse & details:
GET /,GET /index,GET /recipeDetails/:id,GET /viewRecipeFull/:recipeId,GET /getRecipeDetails/:id - Search:
GET /searchIngredients— query string parameters forwarded to Spoonacular - Favorites:
GET /favoriteRecipesPOST /favoriteRecipe— Body:recipeId(server fetches name/URL via Spoonacular)POST /removeFavoriteRecipe— Body:favoriteIdorrecipeId
GET /customRecipes— List user’s custom recipesGET /customizeRecipe/:id— Prefill form from a Spoonacular recipePOST /customizeRecipe/:recipeId— Body:recipeName,instructions,dishType,imageUrl- flags:
vegetarian,vegan,glutenFree,dairyFree(truthy →1) ingredients— array-like payload; each item should provide{ name, amount, unit }
(client forms name inputs likeingredients[0][name], etc.)
GET /updateUser/:userId— Render edit formPOST /updateUser/:userId— Body:firstName,lastName,role(when admin), optionalpassword,confirmPasswordPOST /deleteUser/:userId— Remove a user
GET /dbTest— DB connectivity checkGET /showTables,GET /showTableColumns— DB introspection- Kroger price lookup:
GET /ingredientPrice?name=<term>— Requires Kroger OAuth configuration (getKrogerToken()); defaultfilter.locationId=01400943
Server enables both
express.urlencoded({ extended: true })andexpress.json()to accept form and JSON payloads.
- Passwords: Hashed with bcrypt (see
generate.jsfor seed hash generation). - Sessions:
express-sessionwithSESSION_SECRET. The code setsapp.set('trust proxy', 1)for use behind a reverse proxy. - Authorization: Middleware guards (
isAuthenticated,isUserOrAdmin,isAdmin) protect user/admin areas. - HTML sanitization: When Spoonacular returns HTML instructions, JSDOM is used to strip HTML to text for safer rendering.
- Session store: Configure a persistent store (Redis/MySQL) instead of the default in-memory store.
- Secure cookies: Set
cookie: { httpOnly: true, sameSite: 'lax', secure: true }when serving over HTTPS (and keepapp.set('trust proxy', 1)behind proxies). - CSRF: Consider adding CSRF protection for form POSTs.
- Rate limiting & headers: Add
express-rate-limitandhelmet. - Input validation: Validate body/query parameters (e.g.,
zod/joi/express-validator).
- Provide
.envwith DB + API keys and a strongSESSION_SECRET. - Daily reset: A cron job executes
restore_db.shto reset the database to a demo baseline. Example (3:00 AM daily):0 3 * * * cd /var/www/culinary-otter && bash ./restore_db.sh >> /var/log/culinary-otter-reset.log 2>&1
- Use a process manager (
pm2/systemd). The app listens on port 3000 by default.
- To change demo users, update the seed rows in CulinaryOtter_DB.sql or generate new password hashes:
node generate.js
- Client-side helpers live in public/js/scripts.js (favorite via fetch/AJAX, dynamic ingredient rows).
- Layout/assets: views/partials and public/.