Skip to content

SoundFoodPhygital/menu-api

SoundFood Menu API

License: GPL v3 Tests Lint Poetry Python 3.10+ Flask Code style: Ruff Docker Ask DeepWiki

A Flask RESTful API for managing restaurant menus. This service allows users to create, manage, and organize their restaurant menus with detailed dish attributes including taste profiles, textures, emotions, and visual characteristics.

Features

  • 🔐 JWT-based authentication
  • 📋 Full CRUD operations for menus and dishes
  • 🎨 Rich dish attributes (taste, texture, color, emotions)
  • 👤 User isolation (users can only access their own data)
  • 🛡️ Rate limiting protection
  • 📊 Admin dashboard with analytics
  • 🐳 Docker support

Requirements

  • Python 3.10+
  • Poetry (recommended) or pip

Installation

Using Poetry (Recommended)

# Clone the repository
git clone https://github.com/YOUR_USERNAME/menu-server-demo.git
cd menu-server-demo

# Install dependencies
poetry install

# Initialize the database
poetry run flask --app wsgi db upgrade

# Run the development server
poetry run flask --app wsgi run --debug

Using pip

# Clone the repository
git clone https://github.com/YOUR_USERNAME/menu-server-demo.git
cd menu-server-demo

# Create virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install dependencies
pip install -r requirements.txt

# Initialize the database
flask --app wsgi db upgrade

# Run the development server
flask --app wsgi run --debug

Using Docker

# Create your .env file from example
cp .env.example .env
# Edit .env with your settings (especially SECRET_KEY and ADMIN_PASSWORD)

# Production with SQLite (default)
docker compose up -d

# Production with PostgreSQL
docker compose --profile postgres up -d

# Production with MariaDB
docker compose --profile mariadb up -d

# Development (with hot-reload)
docker compose --profile dev up

# Build manually
docker build -t soundfood-api --target production .

Note: On first startup, the database is automatically initialized with:

  • All required tables
  • Default attributes (emotions, textures, shapes)
  • An admin user (credentials from .env or defaults: admin/admin123)

Configuration

Create a .env file from the example:

cp .env.example .env

Environment Variables

Variable Description Default
SECRET_KEY Flask secret key dev-secret-key
JWT_SECRET_KEY JWT signing key dev-jwt-secret
DATABASE_URL Database connection string sqlite:///instance/project.db
FLASK_ENV Environment (development/production) development
ADMIN_USERNAME Default admin username admin
ADMIN_PASSWORD Default admin password admin123
ADMIN_EMAIL Default admin email admin@example.com
AUTO_INIT_DB Auto-initialize database on startup true

Database Configuration

The application supports multiple database backends:

SQLite (default - good for development and small deployments):

DATABASE_URL=sqlite:///instance/project.db

PostgreSQL (recommended for production):

DATABASE_URL=postgresql://user:password@localhost:5432/soundfood

MariaDB/MySQL:

DATABASE_URL=mysql+pymysql://user:password@localhost:3306/soundfood

Caching

The API uses Flask-Caching to improve performance. By default, it uses in-memory caching (SimpleCache).

Endpoint Cache Duration Description
GET /api/emotions 7 days Emotion attributes (static data)
GET /api/textures 7 days Texture attributes (static data)
GET /api/shapes 7 days Shape attributes (static data)

For production with multiple workers, consider using Redis:

# In your configuration
CACHE_TYPE = "RedisCache"
CACHE_REDIS_URL = "redis://localhost:6379/0"

API Reference

All API endpoints (except authentication and health check) require a valid JWT token in the Authorization header:

Authorization: Bearer <your_token>

Health Check

Check API and database status

GET /api/health

Response (200 - Healthy):

{
  "status": "healthy",
  "database": "connected"
}

Response (503 - Unhealthy):

{
  "status": "unhealthy",
  "database": "disconnected"
}

Authentication

Register a new user

POST /auth/register
Content-Type: application/json

{
  "username": "string",
  "password": "string"
}

Response (201):

{
  "message": "User created successfully",
  "user_id": 1
}

Login

POST /auth/login
Content-Type: application/json

{
  "username": "string",
  "password": "string"
}

Response (200):

{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
  "user_id": 1
}

Logout

POST /auth/logout
Authorization: Bearer <token>

Response (200):

{
  "message": "Successfully logged out"
}

Get current user

GET /auth/me
Authorization: Bearer <token>

Response (200):

{
  "id": 1,
  "username": "john_doe",
  "role": "user",
  "created_at": "2026-01-03T10:30:00+00:00",
  "updated_at": "2026-01-03T12:45:00+00:00"
}

Update user email

PATCH /auth/me/email
Authorization: Bearer <token>
Content-Type: application/json

{
  "email": "newemail@example.com"
}

Response (200):

{
  "message": "Email updated successfully"
}

Error Responses:

  • 400 - Invalid email format
  • 409 - Email already in use

Update user password

PATCH /auth/me/password
Authorization: Bearer <token>
Content-Type: application/json

{
  "current_password": "current_password123",
  "new_password": "new_password456"
}

Response (200):

{
  "message": "Password updated successfully"
}

Error Responses:

  • 400 - Password validation failed (minimum 8 characters, must contain letters and digits)
  • 401 - Current password is incorrect

Delete user account

DELETE /auth/me
Authorization: Bearer <token>
Content-Type: application/json

{
  "password": "your_password123"
}

Response (200):

{
  "message": "Account deleted successfully"
}

Note: Deleting an account will permanently remove all associated menus and dishes.

Error Responses:

  • 401 - Password is incorrect

Menus

List all menus

GET /api/menus
Authorization: Bearer <token>

Response (200):

[
  {
    "id": 1,
    "title": "Lunch Menu",
    "description": "Daily lunch specials",
    "status": "draft",
    "dish_count": 5,
    "created_at": "2026-01-03T10:30:00+00:00",
    "updated_at": "2026-01-03T12:45:00+00:00"
  }
]

Get a specific menu

GET /api/menus/{menu_id}
Authorization: Bearer <token>

Response (200):

{
  "id": 1,
  "title": "Lunch Menu",
  "description": "Daily lunch specials",
  "status": "draft",
  "created_at": "2026-01-03T10:30:00+00:00",
  "updated_at": "2026-01-03T12:45:00+00:00",
  "dishes": [...]
}

Create a menu

POST /api/menus
Authorization: Bearer <token>
Content-Type: application/json

{
  "title": "Dinner Menu",
  "description": "Evening fine dining"
}

Response (201):

{
  "message": "Menu created",
  "id": 2
}

Update a menu

PUT /api/menus/{menu_id}
Authorization: Bearer <token>
Content-Type: application/json

{
  "title": "Updated Title",
  "description": "Updated description",
  "status": "submitted"
}

Note: Status must be either draft or submitted.

Response (200):

{
  "message": "Menu updated"
}

Error Responses:

  • 400 - Invalid status value
  • 403 - Unauthorized to modify this menu
  • 404 - Menu not found

Submit a menu

POST /api/menus/{menu_id}/submit
Authorization: Bearer <token>

Changes the menu status from draft to submitted.

Response (200):

{
  "message": "Menu submitted successfully"
}

Error Responses:

  • 400 - Menu is already submitted
  • 403 - Unauthorized to submit this menu
  • 404 - Menu not found

Delete a menu

DELETE /api/menus/{menu_id}
Authorization: Bearer <token>

Response (200):

{
  "message": "Menu deleted"
}

Dishes

List dishes in a menu

GET /api/menus/{menu_id}/dishes
Authorization: Bearer <token>

Response (200):

[
  {
    "id": 1,
    "name": "Spaghetti Carbonara",
    "description": "Classic Roman pasta",
    "section": "Primi",
    "emotions": [{"id": 1, "description": "Comfort"}],
    "textures": [{"id": 1, "description": "Creamy"}],
    "shapes": [{"id": 1, "description": "Long"}],
    "bitter": 0,
    "salty": 3,
    "sour": 1,
    "sweet": 0,
    "umami": 5,
    "fat": 4,
    "piquant": 1,
    "temperature": 4,
    "colors": ["#F5DEB3", "#FFD700"],
    "created_at": "2026-01-03T10:30:00+00:00",
    "updated_at": "2026-01-03T12:45:00+00:00"
  }
]

Create a dish

POST /api/menus/{menu_id}/dishes
Authorization: Bearer <token>
Content-Type: application/json

{
  "name": "Margherita Pizza",
  "description": "Traditional Neapolitan pizza",
  "section": "Pizze",
  "bitter": 0,
  "salty": 2,
  "sour": 2,
  "sweet": 1,
  "umami": 4,
  "fat": 3,
  "piquant": 0,
  "temperature": 5,
  "color1": "#FF6347",
  "color2": "#FFFFFF",
  "color3": "#228B22",
  "emotion_ids": [1, 2],
  "texture_ids": [3],
  "shape_ids": [2]
}

Response (201):

{
  "message": "Dish created",
  "id": 5
}

Update a dish

PUT /api/dishes/{dish_id}
Authorization: Bearer <token>
Content-Type: application/json

{
  "name": "Updated Dish Name",
  "salty": 4
}

Response (200):

{
  "message": "Dish updated"
}

Delete a dish

DELETE /api/dishes/{dish_id}
Authorization: Bearer <token>

Response (200):

{
  "message": "Dish deleted"
}

Attributes

Get all emotions

GET /api/emotions
Authorization: Bearer <token>

Response (200):

[
  {"id": 1, "description": "Happy"},
  {"id": 2, "description": "Nostalgic"}
]

Get all textures

GET /api/textures
Authorization: Bearer <token>

Response (200):

[
  {"id": 1, "description": "Crunchy"},
  {"id": 2, "description": "Smooth"}
]

Get all shapes

GET /api/shapes
Authorization: Bearer <token>

Response (200):

[
  {"id": 1, "description": "Round"},
  {"id": 2, "description": "Square"}
]

Error Responses

All endpoints return consistent error responses:

{
  "error": "Error message description"
}
Status Code Description
400 Bad Request - Invalid input data
401 Unauthorized - Missing or invalid token
403 Forbidden - Access denied to resource
404 Not Found - Resource doesn't exist
409 Conflict - Resource already exists
429 Too Many Requests - Rate limit exceeded

Admin Dashboard

The application includes an admin dashboard accessible at /admin.

Access Requirements

  • Users must have is_admin=True or is_manager=True to access the dashboard
  • Admins have full CRUD access to all models
  • Managers have read-only access to logs and analytics

Creating an Admin User

Use the Flask CLI to create an admin user:

poetry run flask --app wsgi shell
from app.models import User
admin = User.create(username="admin", password="secure_password", is_admin=True)

Dashboard Features

  • User management
  • Menu and dish overview
  • Request logs and analytics
  • Daily API usage charts

Development

Running Tests

# Run all tests
poetry run pytest

# Run with verbose output
poetry run pytest -v

# Run with coverage report
poetry run pytest --cov=app --cov-report=html

# Run specific test file
poetry run pytest tests/test_auth.py

Code Formatting and Linting

This project uses Ruff for linting and formatting.

# Check for linting issues
poetry run ruff check .

# Fix linting issues automatically
poetry run ruff check --fix .

# Format code
poetry run ruff format .

# Check formatting without making changes
poetry run ruff format --check .

Database Migrations

# Create a new migration
poetry run flask --app wsgi db migrate -m "Description of changes"

# Apply migrations
poetry run flask --app wsgi db upgrade

# Rollback last migration
poetry run flask --app wsgi db downgrade

Project Structure

menu-server-demo/
├── app/
│   ├── __init__.py          # Application factory
│   ├── config.py            # Configuration classes
│   ├── extensions.py        # Flask extensions
│   ├── middleware.py        # Request logging middleware
│   ├── cli.py               # CLI commands
│   ├── api/
│   │   └── __init__.py      # API endpoints
│   ├── auth/
│   │   └── __init__.py      # Authentication endpoints
│   ├── admin/
│   │   ├── __init__.py
│   │   ├── routes.py        # Admin auth routes
│   │   └── views.py         # Flask-Admin views
│   └── models/
│       ├── __init__.py
│       ├── user.py
│       ├── menu.py
│       ├── attributes.py
│       └── request_log.py
├── templates/
│   └── admin/               # Admin dashboard templates
├── migrations/              # Database migrations
├── tests/                   # Test suite
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml
└── README.md

Rate Limits

The API implements rate limiting to prevent abuse:

Endpoint Limit
Register 5/minute
Login 10/minute
Update Email 10/minute
Update Password 10/minute
Delete Account 5/minute
GET endpoints 60/minute
POST endpoints 20-30/minute
DELETE endpoints 10-20/minute

License

This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details.

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes
  4. Run tests and linting (poetry run pytest && poetry run ruff check .)
  5. Commit your changes (git commit -m 'Add amazing feature')
  6. Push to the branch (git push origin feature/amazing-feature)
  7. Open a Pull Request

About

Server to handle menu requests.

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Contributors