From c35ab09ec517a9dbe38c93c6ac91198a2a356ecb Mon Sep 17 00:00:00 2001 From: Duncan Murchison Date: Wed, 7 Jan 2026 10:28:10 -0500 Subject: [PATCH 1/7] Add canonical link to layout and adjust API list styling for better centering --- app/static/style.css | 6 ++++++ app/templates/layout.html | 2 ++ 2 files changed, 8 insertions(+) diff --git a/app/static/style.css b/app/static/style.css index abde03e..b52cfbf 100755 --- a/app/static/style.css +++ b/app/static/style.css @@ -1,3 +1,9 @@ +/* Remove default ul padding/margin for perfect centering */ +#api-list { + padding-left: 0; + margin-left: 0; + list-style: none; +} body::before { content: ""; position: fixed; diff --git a/app/templates/layout.html b/app/templates/layout.html index 43c5dbe..f03b6c2 100755 --- a/app/templates/layout.html +++ b/app/templates/layout.html @@ -8,6 +8,8 @@ {% block title %}API Looter - Open Source API Directory{% endblock %} + a software development and web hosting company offering custom software soluti> + From d15dde1498e5f735d42fb449a241c1bad2c12a6d Mon Sep 17 00:00:00 2001 From: Duncan Murchison Date: Thu, 8 Jan 2026 11:19:46 -0500 Subject: [PATCH 2/7] Add comprehensive documentation and security validation for API contributions - Introduced CODE_OF_CONDUCT.md to establish community guidelines. - Created CONTRIBUTING.md to outline contribution process and API requirements. - Added SECURITY.md detailing security policies and reporting vulnerabilities. - Developed DEVELOPMENT.md for local development setup and workflow. - Established ENVIRONMENTS.md to manage environment-specific configurations. - Implemented validate_apis.py for API validation, ensuring security and quality standards. --- .env.example | 17 + .github/ISSUE_TEMPLATE/add_api.md | 54 ++ .github/ISSUE_TEMPLATE/bug_report.md | 37 ++ .github/PULL_REQUEST_TEMPLATE.md | 50 ++ .github/dependabot.yml | 24 + .github/workflows/security.yml | 56 ++ .github/workflows/validate-pr.yml | 75 +++ .gitignore | 5 +- Dockerfile | 25 +- app/__init__.py | 110 +++- app/api_helpers.py | 24 +- app/data.py | 277 +++++++++ app/forms.py | 8 - app/models.py | 12 - app/reset_db.py | 17 - app/routes.py | 71 ++- app/seed.py | 181 ------ app/static/style.css | 31 + app/templates/api_detail.html | 21 + app/templates/index.html | 18 +- app/templates/layout.html | 2 +- docker-compose.prod.yml | 105 ++++ docker-compose.staging.yml | 90 +++ docker-compose.yml | 23 +- docs/README.md | 187 ++++++ docs/contributing/CODE_OF_CONDUCT.md | 126 ++++ docs/contributing/CONTRIBUTING.md | 227 +++++++ docs/security/SECURITY.md | 137 +++++ docs/setup/DEVELOPMENT.md | 575 ++++++++++++++++++ docs/setup/ENVIRONMENTS.md | 154 +++++ migrations/README | 1 - migrations/alembic.ini | 50 -- migrations/env.py | 113 ---- migrations/script.py.mako | 24 - .../3c3561ca9eff_initial_migration.py | 35 -- .../78ed842c38ba_add_api_key_to_apimodel.py | 32 - requirements.txt | 16 +- run.py | 13 + validate_apis.py | 208 +++++++ 39 files changed, 2675 insertions(+), 556 deletions(-) create mode 100644 .env.example create mode 100644 .github/ISSUE_TEMPLATE/add_api.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/security.yml create mode 100644 .github/workflows/validate-pr.yml create mode 100644 app/data.py delete mode 100755 app/forms.py delete mode 100755 app/models.py delete mode 100755 app/reset_db.py delete mode 100755 app/seed.py create mode 100644 docker-compose.prod.yml create mode 100644 docker-compose.staging.yml create mode 100644 docs/README.md create mode 100644 docs/contributing/CODE_OF_CONDUCT.md create mode 100644 docs/contributing/CONTRIBUTING.md create mode 100644 docs/security/SECURITY.md create mode 100644 docs/setup/DEVELOPMENT.md create mode 100644 docs/setup/ENVIRONMENTS.md delete mode 100755 migrations/README delete mode 100755 migrations/alembic.ini delete mode 100755 migrations/env.py delete mode 100755 migrations/script.py.mako delete mode 100755 migrations/versions/3c3561ca9eff_initial_migration.py delete mode 100755 migrations/versions/78ed842c38ba_add_api_key_to_apimodel.py create mode 100755 validate_apis.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8e8e614 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# api_looter Environment Configuration +# +# IMPORTANT: This repo is public - never commit actual credentials! +# +# WORKFLOW: +# 1. Copy the appropriate template to .env: +# - Development: cp .env.development .env +# - Staging: cp .env.staging .env +# - Production: cp .env.production .env +# 2. Fill in any placeholder values +# 3. The .env file is gitignored and never committed + +# Flask secret key +SECRET_KEY=your-secret-key-here + +# Redis URL (memory:// for dev, redis://... for staging/prod) +REDIS_URL=memory:// diff --git a/.github/ISSUE_TEMPLATE/add_api.md b/.github/ISSUE_TEMPLATE/add_api.md new file mode 100644 index 0000000..f0b12fb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/add_api.md @@ -0,0 +1,54 @@ +--- +name: Add New API +about: Suggest a free API to add to the collection +title: '[API] Add Your API Name' +labels: 'new-api, enhancement' +assignees: '' +--- + +## API Information + +**API Name:** + + +**API Endpoint:** + + +**API Documentation URL:** + + +**Description:** + + + + +**Why should we add this API?** + + + + +## Requirements Check + +- [ ] This API is free to use (no paid subscription required) +- [ ] This API is publicly accessible +- [ ] This API uses HTTPS (not HTTP) +- [ ] This API is family-friendly (no adult/offensive content) +- [ ] I have tested this API and it works +- [ ] This API has educational value for learning about APIs + +## Additional Information + +**Parameters needed:** + + + + +**Example response:** +```json +{ + "example": "paste example response here" +} +``` + +**Category:** + diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..02ec68e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,37 @@ +--- +name: Bug Report +about: Report a bug or issue +title: '[BUG] ' +labels: 'bug' +assignees: '' +--- + +## Describe the Bug + + + + +## To Reproduce +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '...' +3. Fill in '...' +4. See error + +## Expected Behavior + + + + +## Screenshots + + + + +## Environment +- Browser: [e.g. Chrome, Firefox, Safari] +- OS: [e.g. Windows, macOS, Linux] +- Python version (if running locally): [e.g. 3.11] + +## Additional Context + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..fd95bbc --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,50 @@ +## Description + + + + +## Type of Change + + +- [ ] Adding a new API +- [ ] Fixing a bug +- [ ] Updating documentation +- [ ] Security fix +- [ ] Other (please describe): + +## Checklist + +### If adding a new API: +- [ ] Added API entry to `app/data.py` +- [ ] Filled in all required fields (id, name, description, endpoint, parameters, why_use, how_use, category) +- [ ] Endpoint uses HTTPS (not HTTP) +- [ ] Domain is NOT localhost/private IP +- [ ] Tested the API locally with `python run.py` +- [ ] Ran validation script: `python validate_apis.py` āœ… +- [ ] API is free to use (no paid subscription required) +- [ ] API is family-friendly (no adult/offensive content) + +### General: +- [ ] My code follows the project's style guidelines +- [ ] I have tested my changes locally +- [ ] All validation checks pass + +## Testing + + +Tested locally: +- [ ] Started server with `python run.py` +- [ ] Visited http://localhost:8000 +- [ ] Found the API in the list +- [ ] Clicked on the API +- [ ] Filled out parameters (if any) +- [ ] Clicked "Call API" +- [ ] Result displayed correctly + +## Screenshots (if applicable) + + + + +## Additional Notes + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..e8e8abc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +version: 2 +updates: + # Python dependencies + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + labels: + - "dependencies" + - "security" + commit-message: + prefix: "chore" + prefix-development: "chore" + include: "scope" + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" + - "github-actions" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..db89c31 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,56 @@ +name: Security Scanning + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + # Run weekly on Mondays at 9am UTC + - cron: '0 9 * * 1' + +jobs: + dependency-scan: + name: Dependency Scanning + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Snyk security scan + uses: snyk/actions/python@master + continue-on-error: true + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --severity-threshold=high + + - name: Upload Snyk results + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: snyk.sarif + + codeql: + name: CodeQL Analysis + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: python + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml new file mode 100644 index 0000000..6802276 --- /dev/null +++ b/.github/workflows/validate-pr.yml @@ -0,0 +1,75 @@ +name: Validate Pull Request + +on: + pull_request: + branches: [ main ] + paths: + - 'app/data.py' + - 'app/**/*.py' + - 'requirements.txt' + +jobs: + validate-apis: + name: Validate API Data + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run API validation + run: | + python validate_apis.py + + - name: Check for security issues + run: | + echo "āœ… Security validation passed" + + lint: + name: Code Quality + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install ruff + run: pip install ruff + + - name: Run ruff linter + run: ruff check . + + security-scan: + name: Security Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Bandit security scanner + uses: PyCQA/bandit-action@v1 + with: + args: '-r app/ -ll' + + - name: Check for secrets + uses: trufflesecurity/trufflehog@main + with: + path: ./ + base: ${{ github.event.repository.default_branch }} + head: HEAD diff --git a/.gitignore b/.gitignore index b203d26..d7e3779 100755 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,11 @@ __pycache__/ *.pyo *.pyd -# Environment variables +# Environment variables (NEVER commit credentials!) .env +.env.development +.env.staging +.env.production # Virtual environments env/ diff --git a/Dockerfile b/Dockerfile index 704cb80..2832679 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,31 @@ -# Use official Python 3.11 image +# Use official Python 3.11 slim image FROM python:3.11-slim # Set working directory WORKDIR /app -# Install dependencies +# Install system dependencies (curl for health checks) +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements and install Python packages COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -# Copy the rest of the application code +# Copy application code COPY . . -# Expose port 8000 +# Set environment variables +ENV PYTHONUNBUFFERED=1 +ENV PYTHONPATH=/app + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ + CMD curl -f http://localhost:8000/ || exit 1 + +# Expose port EXPOSE 8000 -# Run the app with Gunicorn -CMD ["gunicorn", "-b", "0.0.0.0:8000", "run:app"] +# Run with Gunicorn (4 workers, 120s timeout) +CMD ["gunicorn", "-b", "0.0.0.0:8000", "-w", "4", "--timeout", "120", "run:app"] diff --git a/app/__init__.py b/app/__init__.py index ddd7e19..6ae9889 100755 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,27 +1,111 @@ import os -from flask import Flask -from flask_sqlalchemy import SQLAlchemy -from flask_migrate import Migrate +from flask import Flask, request +from flask_limiter import Limiter +from flask_wtf.csrf import CSRFProtect +from flask_cors import CORS from dotenv import load_dotenv load_dotenv() -db = SQLAlchemy() -migrate = Migrate() +# Auto-generate allowed API domains from data.py for SSRF protection +def get_allowed_domains(): + """Extract allowed domains from API endpoints""" + from urllib.parse import urlparse + from .data import APIS + + domains = set() + for api in APIS: + endpoint = api.get('endpoint', '') + if endpoint: + try: + parsed = urlparse(endpoint) + if parsed.netloc: + domains.add(parsed.netloc) + except: + pass + return domains + +ALLOWED_API_DOMAINS = get_allowed_domains() + + +def get_real_ip(): + """Get real IP address, accounting for proxies and Docker""" + if request.environ.get('HTTP_X_FORWARDED_FOR'): + return request.environ['HTTP_X_FORWARDED_FOR'].split(',')[0].strip() + if request.environ.get('HTTP_X_REAL_IP'): + return request.environ['HTTP_X_REAL_IP'] + return request.environ.get('REMOTE_ADDR', '127.0.0.1') + + +def is_allowed_domain(url): + """Check if URL domain is in whitelist""" + from urllib.parse import urlparse + parsed = urlparse(url) + return parsed.netloc in ALLOWED_API_DOMAINS + + +# Initialize rate limiter +limiter = Limiter( + key_func=get_real_ip, + default_limits=[], + storage_uri=os.environ.get('REDIS_URL', 'memory://') +) + +# Initialize CSRF protection +csrf = CSRFProtect() + def create_app(): app = Flask(__name__) - app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL') - app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # Suppress warning - app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev') # Default to 'dev' if not set + app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev') # Default to 'dev' if not set + + # Initialize extensions + limiter.init_app(app) + csrf.init_app(app) + + # Configure CORS (allow requests from same origin) + CORS(app, supports_credentials=True) - db.init_app(app) - migrate.init_app(app, db) + # Rate limit error handler + @app.errorhandler(429) + def rate_limit_handler(e): + """Handle rate limit exceeded""" + return { + 'error': 'Too many requests. Please try again later.', + 'retry_after': 60 + }, 429 - # Import models so Alembic can detect them - from . import models # noqa: F401 + # Security headers + @app.after_request + def set_security_headers(response): + """Set security headers on all responses""" + response.headers['X-Content-Type-Options'] = 'nosniff' + response.headers['X-Frame-Options'] = 'DENY' + response.headers['X-XSS-Protection'] = '1; mode=block' + response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin' + response.headers['Permissions-Policy'] = 'camera=(), microphone=(), geolocation=()' + + # HSTS in production only + if not app.config.get('TESTING') and not app.debug: + response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains; preload' + + # CSP for HTML pages (not API responses) + if not request.path.startswith('/api'): + response.headers['Content-Security-Policy'] = ( + "default-src 'self'; " + "script-src 'self' 'unsafe-inline'; " # Prism.js needs inline + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: https:; " + "font-src 'self'; " + ) + + return response from .routes import bp as main_bp - app.register_blueprint(main_bp) # Register the main blueprint + app.register_blueprint(main_bp) # Register the main blueprint + + # Exempt main routes from CSRF (API testing doesn't need CSRF protection) + # We already have SameSite=Lax cookies for CSRF protection + csrf.exempt(main_bp) return app diff --git a/app/api_helpers.py b/app/api_helpers.py index 840f322..b80016c 100755 --- a/app/api_helpers.py +++ b/app/api_helpers.py @@ -1,5 +1,6 @@ import json import requests +from app import is_allowed_domain def parse_response(response): @@ -20,7 +21,11 @@ def parse_response(response): # Cat Facts API def handle_cat_facts_api(api, params=None): - response = requests.get(api.endpoint, headers={"Accept": "application/json"}) + endpoint = api['endpoint'] + if not is_allowed_domain(endpoint): + return "This API endpoint is not allowed for security reasons.", "error" + + response = requests.get(endpoint, headers={"Accept": "application/json"}, timeout=10) try: data = response.json() # Extract the "fact" field from the response @@ -31,12 +36,12 @@ def handle_cat_facts_api(api, params=None): # Dog CEO API def handle_dog_ceo_api(api, params=None): - response = requests.get(api.endpoint, headers={"Accept": "application/json"}) + response = requests.get(api['endpoint'], headers={"Accept": "application/json"}, timeout=10) return parse_response(response) # DogAPI def handle_dog_api(api, params=None): - response = requests.get(api.endpoint, headers={"Accept": "application/json"}) + response = requests.get(api['endpoint'], headers={"Accept": "application/json"}, timeout=10) try: data = response.json() # Extract the "body" field from the first item in "data" @@ -70,7 +75,7 @@ def parse_jokeapi_response(response): # Advice Slip API def handle_advice_slip_api(api, params=None): - response = requests.get(api.endpoint, headers={"Accept": "application/json"}) + response = requests.get(api['endpoint'], headers={"Accept": "application/json"}, timeout=10) try: data = response.json() # Extract the "advice" field from the "slip" object @@ -81,7 +86,7 @@ def handle_advice_slip_api(api, params=None): # Dad Jokes API def handle_dad_jokes_api(api, params=None): - response = requests.get(api.endpoint, headers={"Accept": "application/json"}) + response = requests.get(api['endpoint'], headers={"Accept": "application/json"}, timeout=10) try: data = response.json() # Extract the "joke" field from the response @@ -91,7 +96,7 @@ def handle_dad_jokes_api(api, params=None): return "Failed to parse Dad Jokes API response.", "text" def handle_kanye_rest_api(api, params=None): - response = requests.get(api.endpoint, headers={"Accept": "application/json"}) + response = requests.get(api['endpoint'], headers={"Accept": "application/json"}, timeout=10) try: data = response.json() # Extract the "quote" field from the response @@ -101,5 +106,10 @@ def handle_kanye_rest_api(api, params=None): return "Failed to parse Kanye Rest API response.", "text" def handle_default_api(api, params=None): - response = requests.get(api.endpoint, params=params, headers={"Accept": "application/json"}) + endpoint = api['endpoint'] + # SSRF Protection: Check if domain is whitelisted + if not is_allowed_domain(endpoint): + return "This API endpoint is not allowed for security reasons.", "error" + + response = requests.get(endpoint, params=params, headers={"Accept": "application/json"}, timeout=10) return parse_response(response) diff --git a/app/data.py b/app/data.py new file mode 100644 index 0000000..5d9f1b9 --- /dev/null +++ b/app/data.py @@ -0,0 +1,277 @@ +""" +Static API data structure for api_looter. +No database needed - all API information stored here. +""" + +APIS = [ + { + "id": 1, + "name": "Dog CEO", + "description": "Random pictures of dogs.", + "endpoint": "https://dog.ceo/api/breeds/image/random", + "parameters": [], + "why_use": "Great for placeholder images, testing image handling, or building pet-related apps.", + "how_use": "Perfect for learning HTTP requests - no API key needed, returns JSON with image URL. Ideal for beginners practicing API calls.", + "category": "Images" + }, + { + "id": 2, + "name": "Cat Facts", + "description": "Get random cat facts.", + "endpoint": "https://catfact.ninja/fact", + "parameters": [], + "why_use": "Learn JSON parsing and handling text responses from APIs.", + "how_use": "Simple GET request returns random cat facts - ideal first API for beginners. No authentication required.", + "category": "Fun" + }, + { + "id": 3, + "name": "OpenWeatherMap", + "description": "Get current weather data.", + "endpoint": "https://api.openweathermap.org/data/2.5/weather", + "parameters": [ + {"name": "q", "label": "City", "type": "text", "required": True}, + {"name": "appid", "label": "API Key", "type": "text", "required": True} + ], + "why_use": "Learn how to work with APIs that require authentication and handle query parameters.", + "how_use": "Demonstrates API key usage and parameter passing. Common in weather apps, travel sites, and IoT projects.", + "category": "Data" + }, + { + "id": 4, + "name": "Advice Slip", + "description": "Random life advice.", + "endpoint": "https://api.adviceslip.com/advice", + "parameters": [], + "why_use": "Simple API perfect for practicing JSON data extraction and response handling.", + "how_use": "Returns motivational advice - great for learning apps, bots, or daily inspiration features.", + "category": "Fun" + }, + { + "id": 5, + "name": "JokeAPI", + "description": "Programming and general jokes.", + "endpoint": "https://v2.jokeapi.dev/joke", + "parameters": [ + { + "name": "category", + "label": "Category", + "type": "select", + "required": True, + "options": [ + {"value": "programming", "label": "Programming"}, + {"value": "misc", "label": "Miscellaneous"}, + {"value": "pun", "label": "Pun"}, + {"value": "spooky", "label": "Spooky"}, + {"value": "christmas", "label": "Christmas"} + ] + }, + { + "name": "type", + "label": "Type", + "type": "select", + "required": False, + "options": [ + {"value": "single", "label": "Single"}, + {"value": "twopart", "label": "Two-Part"} + ] + } + ], + "why_use": "Learn parameter handling with dropdown options and conditional response structures.", + "how_use": "Popular for Slack bots, Discord bots, and entertainment apps. Shows how to handle multiple response formats.", + "category": "Fun" + }, + { + "id": 6, + "name": "CoinGecko", + "description": "Cryptocurrency prices and info.", + "endpoint": "https://api.coingecko.com/api/v3/simple/price", + "parameters": [ + { + "name": "ids", + "label": "Coin", + "type": "select", + "required": True, + "options": [ + {"value": "bitcoin", "label": "Bitcoin"}, + {"value": "ethereum", "label": "Ethereum"}, + {"value": "dogecoin", "label": "Dogecoin"}, + {"value": "litecoin", "label": "Litecoin"}, + {"value": "cardano", "label": "Cardano"}, + {"value": "solana", "label": "Solana"}, + {"value": "ripple", "label": "Ripple"}, + {"value": "polkadot", "label": "Polkadot"}, + {"value": "tron", "label": "Tron"} + ] + }, + { + "name": "vs_currencies", + "label": "Currency", + "type": "select", + "required": True, + "options": [ + {"value": "usd", "label": "USD"}, + {"value": "eur", "label": "EUR"}, + {"value": "gbp", "label": "GBP"}, + {"value": "jpy", "label": "JPY"}, + {"value": "aud", "label": "AUD"} + ] + } + ], + "why_use": "Practice working with financial data APIs and real-time price information.", + "how_use": "Used in crypto portfolio trackers, price alert apps, and trading dashboards. No API key required for basic usage.", + "category": "Cryptocurrency" + }, + { + "id": 7, + "name": "Genderize", + "description": "Predict gender from a first name.", + "endpoint": "https://api.genderize.io", + "parameters": [ + {"name": "name", "label": "Name", "type": "text", "required": True} + ], + "why_use": "Learn about machine learning prediction APIs and probability-based responses.", + "how_use": "Useful for data analysis, user profiling, and demographic research. Returns gender probability scores.", + "category": "Data" + }, + { + "id": 8, + "name": "Agify", + "description": "Predict age from a name.", + "endpoint": "https://api.agify.io", + "parameters": [ + {"name": "name", "label": "Name", "type": "text", "required": True} + ], + "why_use": "Understand prediction APIs and statistical estimation from names.", + "how_use": "Used in demographic analysis, marketing research, and data enrichment tools.", + "category": "Data" + }, + { + "id": 9, + "name": "Nationalize", + "description": "Predict nationality from a name.", + "endpoint": "https://api.nationalize.io", + "parameters": [ + {"name": "name", "label": "Name", "type": "text", "required": True} + ], + "why_use": "Practice handling multiple prediction results with probability scores.", + "how_use": "Helps with internationalization, market research, and understanding name origins. Returns multiple country probabilities.", + "category": "Data" + }, + { + "id": 10, + "name": "DogAPI", + "description": "Get random dog facts.", + "endpoint": "https://dogapi.dog/api/v2/facts", + "parameters": [], + "why_use": "Learn to navigate nested JSON responses and extract specific data fields.", + "how_use": "Great for pet apps, educational content, or practicing JSON parsing with complex structures.", + "category": "Fun" + }, + { + "id": 11, + "name": "Numbers API", + "description": "Trivia and facts about numbers.", + "endpoint": "http://numbersapi.com/random/trivia", + "parameters": [], + "why_use": "Simple text-based API for learning basic HTTP requests and plain text responses.", + "how_use": "Fun facts for educational apps, trivia games, or daily number facts. Returns plain text instead of JSON.", + "category": "Fun" + }, + { + "id": 12, + "name": "OpenLibrary", + "description": "Book data and cover art.", + "endpoint": "https://openlibrary.org/search.json", + "parameters": [ + {"name": "q", "label": "Search Query", "type": "text", "required": True} + ], + "why_use": "Practice working with large, complex JSON responses and search functionality.", + "how_use": "Essential for book apps, library systems, reading trackers, and educational projects. Free and extensive book database.", + "category": "Data" + }, + { + "id": 13, + "name": "Kanye Rest", + "description": "Get a random Kanye West quote.", + "endpoint": "https://api.kanye.rest", + "parameters": [], + "why_use": "Extremely simple API perfect for your very first API call - just one endpoint, no parameters.", + "how_use": "Popular for meme apps, quote generators, and teaching API basics. Instant success guaranteed!", + "category": "Fun" + }, + { + "id": 14, + "name": "Dad Jokes", + "description": "Get a dad joke.", + "endpoint": "https://icanhazdadjoke.com/", + "parameters": [], + "why_use": "Learn about content negotiation - API returns different formats based on Accept header.", + "how_use": "Common in Slack bots, entertainment apps, and icebreaker tools. Shows how headers affect API responses.", + "category": "Fun" + } +] + + +def get_all_apis(): + """ + Return all APIs sorted by name. + + Returns: + list: Sorted list of API dictionaries + """ + return sorted(APIS, key=lambda x: x['name']) + + +def get_api_by_id(api_id): + """ + Get single API by ID. + + Args: + api_id (int): The API ID to search for + + Returns: + dict or None: API dictionary if found, None otherwise + """ + return next((api for api in APIS if api['id'] == api_id), None) + + +def search_apis(query): + """ + Search APIs by name or description. + + Args: + query (str): Search query string + + Returns: + list: List of matching API dictionaries + """ + query = query.lower() + return [ + api for api in APIS + if query in api['name'].lower() or query in api['description'].lower() + ] + + +def get_apis_by_category(category): + """ + Get all APIs in a specific category. + + Args: + category (str): Category name (e.g., 'Fun', 'Data', 'Images') + + Returns: + list: List of APIs in the category + """ + return [api for api in APIS if api.get('category') == category] + + +def get_all_categories(): + """ + Get list of unique categories. + + Returns: + list: Sorted list of category names + """ + categories = {api.get('category', 'Other') for api in APIS} + return sorted(categories) diff --git a/app/forms.py b/app/forms.py deleted file mode 100755 index ed2dc00..0000000 --- a/app/forms.py +++ /dev/null @@ -1,8 +0,0 @@ -from flask_wtf import FlaskForm -from wtforms import StringField, TextAreaField, SubmitField -from wtforms.validators import DataRequired - -class ApiCallForm(FlaskForm): - api_key = StringField('API Key', validators=[DataRequired()]) - parameters = TextAreaField('Parameters', validators=[DataRequired()]) - submit = SubmitField('Make API Call') diff --git a/app/models.py b/app/models.py deleted file mode 100755 index 72de8c5..0000000 --- a/app/models.py +++ /dev/null @@ -1,12 +0,0 @@ -from . import db - -class APIModel(db.Model): - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(100), nullable=False) - description = db.Column(db.Text, nullable=True) - endpoint = db.Column(db.String(200), nullable=False) - parameters = db.Column(db.JSON, nullable=True) - api_key = db.Column(db.String(255), nullable=True) # <-- Add this line - - def __repr__(self): - return f'' diff --git a/app/reset_db.py b/app/reset_db.py deleted file mode 100755 index e71f05a..0000000 --- a/app/reset_db.py +++ /dev/null @@ -1,17 +0,0 @@ -from app import create_app, db -from app.models import APIModel - -import sys -import os - -# Add the parent directory to the Python path -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from app import create_app, db -from app.models import APIModel - -app = create_app() -with app.app_context(): - db.session.query(APIModel).delete() - db.session.commit() - print("All APIs have been removed from the database.") diff --git a/app/routes.py b/app/routes.py index 4a4625c..677a1ce 100755 --- a/app/routes.py +++ b/app/routes.py @@ -1,5 +1,6 @@ -from flask import Blueprint, render_template, request -from .models import APIModel +from flask import Blueprint, render_template, request, abort, jsonify +from app import limiter +from .data import get_all_apis, get_api_by_id from .api_helpers import ( handle_cat_facts_api, handle_dog_ceo_api, @@ -15,32 +16,60 @@ @bp.route('/') def index(): - apis = APIModel.query.order_by(APIModel.name.asc()).all() + apis = get_all_apis() return render_template('index.html', apis=apis) @bp.route('/api/', methods=['GET', 'POST']) +@limiter.limit("30 per minute", methods=['POST']) # Only rate limit POST requests def api_detail(api_id): - api = APIModel.query.get_or_404(api_id) + api = get_api_by_id(api_id) + if not api: + abort(404) + result = None result_type = None + + # Only process POST requests (API calls) if request.method == 'POST': - params = {param["name"]: request.form.get(param["name"]) for param in api.parameters} if api.parameters else None + + # Build parameters from form data + params = {} + if api.get("parameters"): + for param in api["parameters"]: + value = request.form.get(param["name"]) + if value: + # Input validation: prevent DoS attacks with very long parameters + if len(str(value)) > 500: + return render_template( + 'api_detail.html', + api=api, + result="Parameter too long (max 500 characters)", + result_type="error" + ) + params[param["name"]] = value # Dispatch to helper based on API name - if api.name.lower() == "cat facts": - result, result_type = handle_cat_facts_api(api) - elif api.name.lower() == "dog ceo": - result, result_type = handle_dog_ceo_api(api) - elif api.name.lower() == "jokeapi": - result, result_type = handle_jokeapi(api, params) - elif api.name.lower() == "dogapi": - result, result_type = handle_dog_api(api, params) - elif api.name.lower() == "advice slip": - result, result_type = handle_advice_slip_api(api) - elif api.name.lower() == "kanye rest": - result, result_type = handle_kanye_rest_api(api) - elif api.name.lower() == "dad jokes": - result, result_type = handle_dad_jokes_api(api) - else: - result, result_type = handle_default_api(api, params) + api_name = api.get('name', '').lower() + try: + if api_name == "cat facts": + result, result_type = handle_cat_facts_api(api) + elif api_name == "dog ceo": + result, result_type = handle_dog_ceo_api(api) + elif api_name == "jokeapi": + result, result_type = handle_jokeapi(api, params) + elif api_name == "dogapi": + result, result_type = handle_dog_api(api, params) + elif api_name == "advice slip": + result, result_type = handle_advice_slip_api(api) + elif api_name == "kanye rest": + result, result_type = handle_kanye_rest_api(api) + elif api_name == "dad jokes": + result, result_type = handle_dad_jokes_api(api) + else: + result, result_type = handle_default_api(api, params) + except Exception as e: + # Don't expose internal errors to users + result = "An error occurred while calling the API. Please try again." + result_type = "error" + return render_template('api_detail.html', api=api, result=result, result_type=result_type) diff --git a/app/seed.py b/app/seed.py deleted file mode 100755 index 4ba76a4..0000000 --- a/app/seed.py +++ /dev/null @@ -1,181 +0,0 @@ -from app import create_app, db -from app.models import APIModel - - -def seed_apis(): - apis = [ - # Dog CEO API - APIModel( - name="Dog CEO", - description="Random pictures of dogs.", - endpoint="https://dog.ceo/api/breeds/image/random", - parameters=[] - ), - # Cat Facts API - APIModel( - name="Cat Facts", - description="Get random cat facts.", - endpoint="https://catfact.ninja/fact", - parameters=[] - ), - # OpenWeatherMap API - APIModel( - name="OpenWeatherMap", - description="Get current weather data.", - endpoint="https://api.openweathermap.org/data/2.5/weather", - parameters=[ - {"name": "q", "label": "City", "type": "text", "required": True}, - {"name": "appid", "label": "API Key", "type": "text", "required": True} - ] - ), - # Advice Slip API - APIModel( - name="Advice Slip", - description="Random life advice.", - endpoint="https://api.adviceslip.com/advice", - parameters=[] - ), - # JokeAPI - APIModel( - name="JokeAPI", - description="Programming and general jokes.", - endpoint="https://v2.jokeapi.dev/joke", - parameters=[ - { - "name": "category", - "label": "Category", - "type": "select", - "required": True, - "options": [ - {"value": "programming", "label": "Programming"}, - {"value": "misc", "label": "Miscellaneous"}, - {"value": "dark", "label": "Dark"}, - {"value": "pun", "label": "Pun"}, - {"value": "spooky", "label": "Spooky"}, - {"value": "christmas", "label": "Christmas"} - ] - }, - { - "name": "type", - "label": "Type", - "type": "select", - "required": False, - "options": [ - {"value": "single", "label": "Single"}, - {"value": "twopart", "label": "Two-Part"} - ] - } - ] - ), - # CoinGecko API - APIModel( - name="CoinGecko", - description="Cryptocurrency prices and info.", - endpoint="https://api.coingecko.com/api/v3/simple/price", - parameters=[ - { - "name": "ids", - "label": "Coin", - "type": "select", - "required": True, - "options": [ - {"value": "bitcoin", "label": "Bitcoin"}, - {"value": "ethereum", "label": "Ethereum"}, - {"value": "dogecoin", "label": "Dogecoin"}, - {"value": "litecoin", "label": "Litecoin"}, - {"value": "cardano", "label": "Cardano"}, - {"value": "solana", "label": "Solana"}, - {"value": "ripple", "label": "Ripple"}, - {"value": "polkadot", "label": "Polkadot"}, - {"value": "tron", "label": "Tron"} - ] - }, - { - "name": "vs_currencies", - "label": "Currency", - "type": "select", - "required": True, - "options": [ - {"value": "usd", "label": "USD"}, - {"value": "eur", "label": "EUR"}, - {"value": "gbp", "label": "GBP"}, - {"value": "jpy", "label": "JPY"}, - {"value": "aud", "label": "AUD"} - ] - } - ] - ), - # Genderize API - APIModel( - name="Genderize", - description="Predict gender from a first name.", - endpoint="https://api.genderize.io", - parameters=[ - {"name": "name", "label": "Name", "type": "text", "required": True} - ] - ), - # Agify API - APIModel( - name="Agify", - description="Predict age from a name.", - endpoint="https://api.agify.io", - parameters=[ - {"name": "name", "label": "Name", "type": "text", "required": True} - ] - ), - # Nationalize API - APIModel( - name="Nationalize", - description="Predict nationality from a name.", - endpoint="https://api.nationalize.io", - parameters=[ - {"name": "name", "label": "Name", "type": "text", "required": True} - ] - ), - # DogAPI - APIModel( - name="DogAPI", - description="Get random dog facts.", - endpoint="https://dogapi.dog/api/v2/facts", - parameters=[] - ), - # Numbers API - APIModel( - name="Numbers API", - description="Trivia and facts about numbers.", - endpoint="http://numbersapi.com/random/trivia", - parameters=[] - ), - # Open Library API - APIModel( - name="OpenLibrary", - description="Book data and cover art.", - endpoint="https://openlibrary.org/search.json", - parameters=[ - {"name": "q", "label": "Search Query", "type": "text", "required": True} - ] - ), - # Kanye Rest API - APIModel( - name="Kanye Rest", - description="Get a random Kanye West quote.", - endpoint="https://api.kanye.rest", - parameters=[] - ), - # Dad Jokes API - APIModel( - name="Dad Jokes", - description="Get a dad joke.", - endpoint="https://icanhazdadjoke.com/", - parameters=[] - ), - ] - db.session.bulk_save_objects(apis) - db.session.commit() - print("Database seeded with APIs.") - - -if __name__ == "__main__": - app = create_app() - with app.app_context(): - seed_apis() diff --git a/app/static/style.css b/app/static/style.css index b52cfbf..097493f 100755 --- a/app/static/style.css +++ b/app/static/style.css @@ -244,6 +244,33 @@ button:hover, .back-btn:hover { } } +/* Educational Content Section */ +.api-education { + background-color: #1e2327; + border-left: 4px solid var(--accent-green); + padding: 1.5rem; + margin-bottom: 2rem; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +.api-education h3 { + color: var(--accent-green); + font-size: 1.1rem; + margin-top: 0; + margin-bottom: 0.5rem; +} + +.api-education p { + color: #c9d1d9; + line-height: 1.6; + margin-bottom: 0.75rem; +} + +.api-education p:last-child { + margin-bottom: 0; +} + @media (max-width: 600px) { .api-detail-center, .container { max-width: 98vw; @@ -252,4 +279,8 @@ button:hover, .back-btn:hover { input, select, .joke-result, pre { max-width: 98vw; } + .api-education { + padding: 1rem; + margin-bottom: 1.5rem; + } } diff --git a/app/templates/api_detail.html b/app/templates/api_detail.html index 477126c..c885de8 100755 --- a/app/templates/api_detail.html +++ b/app/templates/api_detail.html @@ -6,6 +6,27 @@

{{ api.name }}

{{ api.description }}

Endpoint: {{ api.endpoint }}

+ + {% if api.why_use or api.how_use %} +
+ {% if api.why_use %} +

šŸ’” Why Use This API?

+

{{ api.why_use }}

+ {% endif %} + + {% if api.how_use %} +

šŸ”§ How Developers Use It

+

{{ api.how_use }}

+ {% endif %} + + {% if api.category %} +

+ Category: {{ api.category }} +

+ {% endif %} +
+ {% endif %} +
{% if api.parameters and api.parameters|length > 0 %}
diff --git a/app/templates/index.html b/app/templates/index.html index a60b470..0782b45 100755 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,17 +1,17 @@ {% extends "layout.html" %} {% block content %}
-

Available APIs

-
- - Commit to a growing list and community of usable APIs! - -
- - ⭐ Star or Contribute on GitHub +

Free APIs to Explore

+
- +
    {% for api in apis %}
  • diff --git a/app/templates/layout.html b/app/templates/layout.html index f03b6c2..db5848a 100755 --- a/app/templates/layout.html +++ b/app/templates/layout.html @@ -8,7 +8,7 @@ {% block title %}API Looter - Open Source API Directory{% endblock %} - a software development and web hosting company offering custom software soluti> + diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..64ac918 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,105 @@ +version: '3.8' + +networks: + proxy_network: + driver: bridge + ipam: + config: + - subnet: 172.40.0.0/24 + backend_network: + driver: bridge + ipam: + config: + - subnet: 172.41.0.0/24 + +services: + redis: + image: redis:7-alpine + container_name: api_looter_redis_prod + restart: always + command: > + redis-server + --requirepass ${REDIS_PASSWORD} + --maxmemory 128mb + --maxmemory-policy allkeys-lru + --save 60 1000 + --appendonly yes + --appendfsync everysec + volumes: + - redis_data:/data + networks: + - backend_network + expose: + - "6379" + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + interval: 10s + timeout: 3s + retries: 3 + deploy: + resources: + limits: + memory: 256M + cpus: '0.25' + security_opt: + - no-new-privileges:true + + backend: + build: . + container_name: api_looter_backend_prod + restart: always + environment: + - SECRET_KEY=${SECRET_KEY} + - REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379/0 + - FLASK_ENV=production + networks: + - proxy_network + - backend_network + expose: + - "8000" + depends_on: + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + cap_add: + - NET_BIND_SERVICE + deploy: + resources: + limits: + memory: 512M + cpus: '0.5' + read_only: true + tmpfs: + - /tmp + - /app/__pycache__ + + cloudflared: + image: cloudflare/cloudflared:latest + container_name: api_looter_cloudflared + restart: always + command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TUNNEL_TOKEN} + networks: + - proxy_network + depends_on: + backend: + condition: service_healthy + security_opt: + - no-new-privileges:true + deploy: + resources: + limits: + memory: 128M + cpus: '0.1' + +volumes: + redis_data: + driver: local diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml new file mode 100644 index 0000000..1be6a3f --- /dev/null +++ b/docker-compose.staging.yml @@ -0,0 +1,90 @@ +version: '3.8' + +networks: + proxy_network: + driver: bridge + ipam: + config: + - subnet: 172.42.0.0/24 + backend_network: + driver: bridge + ipam: + config: + - subnet: 172.43.0.0/24 + +services: + redis: + image: redis:7-alpine + container_name: api_looter_redis_staging + restart: always + command: > + redis-server + --requirepass ${REDIS_PASSWORD} + --maxmemory 128mb + --maxmemory-policy allkeys-lru + --save 60 1000 + --appendonly yes + volumes: + - redis_data:/data + networks: + - backend_network + expose: + - "6379" + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + interval: 10s + timeout: 3s + retries: 3 + deploy: + resources: + limits: + memory: 256M + cpus: '0.25' + + backend: + build: . + container_name: api_looter_backend_staging + restart: always + environment: + - SECRET_KEY=${SECRET_KEY} + - REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379/0 + - FLASK_ENV=staging + networks: + - proxy_network + - backend_network + expose: + - "8000" + depends_on: + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + deploy: + resources: + limits: + memory: 512M + cpus: '0.5' + + cloudflared: + image: cloudflare/cloudflared:latest + container_name: api_looter_cloudflared_staging + restart: always + command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TUNNEL_TOKEN} + networks: + - proxy_network + depends_on: + backend: + condition: service_healthy + deploy: + resources: + limits: + memory: 128M + cpus: '0.1' + +volumes: + redis_data: + driver: local diff --git a/docker-compose.yml b/docker-compose.yml index e7a0fab..b60d988 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,25 +3,14 @@ version: '3.8' services: web: build: . - container_name: api_looter_web - command: gunicorn -b 0.0.0.0:8000 run:app - # command: flask run --host=0.0.0.0 --port=8000 + container_name: api_looter_dev + command: flask run --host=0.0.0.0 --port=8000 ports: - - "8000:8000" + - "5000:8000" environment: - DATABASE_URL: ${DATABASE_URL} - SECRET_KEY: ${SECRET_KEY} + FLASK_APP: run.py FLASK_ENV: development + SECRET_KEY: ${SECRET_KEY} + REDIS_URL: memory:// # In-memory for development (no Redis container needed) volumes: - .:/app - - watchtower: - image: containrrr/watchtower - container_name: watchtower - restart: always - volumes: - - /var/run/docker.sock:/var/run/docker.sock - command: --cleanup --schedule "0 0 * * *" - -volumes: - pgdata: diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..d91087c --- /dev/null +++ b/docs/README.md @@ -0,0 +1,187 @@ +# API Looter - Documentation + +Educational collection of free APIs with preview functionality and enterprise-grade security. + +--- + +## šŸ“š Documentation Index + +### Getting Started +- **[Development Guide](setup/DEVELOPMENT.md)** - Local development setup and workflow +- **[Production Deployment](setup/PRODUCTION.md)** - Deploying to production with Docker +- **[Environment Setup](setup/ENVIRONMENTS.md)** - Managing dev/staging/production environments + +### Security +- **[Security Documentation](security/SECURITY.md)** - Complete security architecture + - SSRF Protection (Domain Whitelist) + - Rate Limiting + - Security Headers (CSP, HSTS, etc.) + - Input Validation + - And more... + +### Contributing +- **[Contributing Guide](contributing/CONTRIBUTING.md)** - How to add new APIs +- **[Code of Conduct](contributing/CODE_OF_CONDUCT.md)** - Community standards + +### CI/CD +- **[GitHub Actions](ci-cd/WORKFLOWS.md)** - Automated validation and security scanning + +--- + +## šŸš€ Quick Links + +### Development + +```bash +# Start development server (no Docker needed) +cp .env.development .env +python run.py +``` + +**App:** http://localhost:8000 + +### Contributing an API + +**Just edit one file!** - `app/data.py` + +The domain whitelist auto-extracts from your endpoint. See [CONTRIBUTING.md](contributing/CONTRIBUTING.md) for details. + +--- + +## šŸ” Security Highlights + +This application implements **enterprise-grade security** despite being a simple educational tool: + +- āœ… **SSRF Protection** - Auto-generated domain whitelist +- āœ… **Rate Limiting** - 30 requests/minute (Redis-backed) +- āœ… **Security Headers** - CSP, HSTS, X-Frame-Options, and more +- āœ… **Input Validation** - Parameter length limits, type checking +- āœ… **No Database** - Zero SQL injection risk (static data) +- āœ… **Automated Security Scanning** - CodeQL, Dependabot, Bandit + +**Security Score:** Enterprise-grade for a simple educational app + +See [Security Documentation](security/SECURITY.md) for complete details. + +--- + +## šŸ“Š Architecture + +### Technology Stack + +**Backend:** +- Flask (Python web framework) +- Flask-Limiter (rate limiting) +- Flask-WTF (CSRF protection) +- Gunicorn (production WSGI server) +- Redis (rate limit storage) + +**Infrastructure:** +- Docker + Docker Compose +- Cloudflare Tunnel (HTTPS, DDoS protection) +- Static data structure (no database) + +### Project Structure + +``` +api_looter/ +ā”œā”€ā”€ app/ +│ ā”œā”€ā”€ __init__.py # Flask app factory + security +│ ā”œā”€ā”€ data.py # Static API data (contributors edit this!) +│ ā”œā”€ā”€ routes.py # API endpoints +│ ā”œā”€ā”€ api_helpers.py # API call handlers +│ ā”œā”€ā”€ static/ # CSS, JS, images +│ └── templates/ # HTML templates +│ +ā”œā”€ā”€ docs/ # Documentation (you are here!) +ā”œā”€ā”€ .github/workflows/ # CI/CD automation +ā”œā”€ā”€ validate_apis.py # Security validation script +ā”œā”€ā”€ docker-compose.*.yml # Docker configurations +└── run.py # Application entry point +``` + +--- + +## šŸŽÆ Use Cases + +This platform is ideal for: + +- **Learning APIs** - Understand how APIs work by testing real endpoints +- **API Discovery** - Find free APIs for your projects +- **Educational Projects** - Reference implementation for Flask security +- **Beginner-Friendly** - No API keys required, instant testing +- **Open Source Reference** - Enterprise security patterns for simple apps + +--- + +## šŸ›”ļø OWASP Top 10 Coverage + +This application addresses relevant OWASP Top 10 (2021) vulnerabilities: + +1. **Broken Access Control** - Rate limiting prevents abuse +2. **Cryptographic Failures** - HTTPS, secure cookies, HSTS +3. **Injection** - No database (static data), input validation +4. **Insecure Design** - Defense-in-depth architecture +5. **Security Misconfiguration** - Comprehensive security headers +6. **Vulnerable Components** - Dependabot auto-updates +7. **Authentication Failures** - Rate limiting on all endpoints +8. **Data Integrity Failures** - Input validation, CSRF protection +9. **Logging Failures** - Security event logging (production) +10. **SSRF** - Domain whitelist, auto-extracted from endpoints + +See [Security Documentation](security/SECURITY.md) for implementation details. + +--- + +## šŸ“Š API Collection + +Current APIs: **14 free APIs** + +**Categories:** +- šŸ–¼ļø **Images** - Dog CEO, Random Fox +- šŸŽ‰ **Fun** - Cat Facts, Dad Jokes, Kanye Quotes, Advice Slip, Jokes +- šŸ“š **Data** - Numbers API, Open Library, Genderize, Agify, Nationalize +- šŸ’° **Cryptocurrency** - CoinGecko + +All APIs are: +- āœ… Free to use (no paid subscription) +- āœ… Publicly accessible (no API key required) +- āœ… HTTPS-only (except Numbers API - HTTP allowed) +- āœ… Family-friendly +- āœ… Educational value + +--- + +## šŸ¤ Contributing + +**Want to add an API?** + +1. Edit `app/data.py` (just one file!) +2. Add your API to the `APIS` list +3. Submit a PR +4. Automated validation checks security requirements +5. Done! + +The domain whitelist auto-extracts from your endpoint - no manual updates needed. + +See [CONTRIBUTING.md](contributing/CONTRIBUTING.md) for step-by-step guide. + +--- + +## šŸ“ž Support + +- **Security Issues:** See [SECURITY.md](security/SECURITY.md) +- **Bug Reports:** GitHub Issues +- **Feature Requests:** GitHub Issues +- **Questions:** GitHub Discussions + +--- + +## šŸ“ License + +See LICENSE file for details. + +--- + +*Documentation Last Updated: January 2026* +*Application Version: 2.0 (Database-less refactor)* diff --git a/docs/contributing/CODE_OF_CONDUCT.md b/docs/contributing/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..bb64bfc --- /dev/null +++ b/docs/contributing/CODE_OF_CONDUCT.md @@ -0,0 +1,126 @@ +# Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment: + +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes +* Focusing on what is best for the overall community +* Showing empathy towards other community members +* Helping beginners learn about APIs and web development + +Examples of unacceptable behavior: + +* The use of sexualized language or imagery, and sexual attention or advances +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information without explicit permission +* Submitting malicious APIs or code with security vulnerabilities +* Other conduct which could reasonably be considered inappropriate + +## Educational Purpose + +api_looter is an educational project designed to help beginners learn about APIs. +We expect all contributors to: + +* Be patient with beginners asking questions +* Provide helpful, constructive feedback on contributions +* Focus on teaching and learning +* Maintain a welcoming environment for all skill levels + +## Enforcement Responsibilities + +Project maintainers are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned with this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, including: + +* GitHub repository (issues, PRs, discussions) +* Project communication channels +* Public spaces when representing the project + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the project team at: + +**support@computeranything.dev** + +All complaints will be reviewed and investigated promptly and fairly. + +All project team members are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Project maintainers will follow these Community Impact Guidelines: + +### 1. Correction +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome. + +**Consequence**: A private, written warning, providing clarity around the nature +of the violation and an explanation of why the behavior was inappropriate. + +### 2. Warning +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved for a specified period of time. + +### 3. Temporary Ban +**Community Impact**: A serious violation of community standards. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. + +### 4. Permanent Ban +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment, or +aggression toward individuals or classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Security Issues + +For security vulnerabilities, please follow the process outlined in +[SECURITY.md](SECURITY.md). Do not create public issues for security problems. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/docs/contributing/CONTRIBUTING.md b/docs/contributing/CONTRIBUTING.md new file mode 100644 index 0000000..8db8cde --- /dev/null +++ b/docs/contributing/CONTRIBUTING.md @@ -0,0 +1,227 @@ +# Contributing to api_looter + +Thank you for your interest in contributing! šŸŽ‰ + +## How to Contribute + +### Adding a New API (Most Common) + +**This is the easiest way to contribute!** Just edit one file. + +1. **Fork the repository** on GitHub + +2. **Clone your fork**: + ```bash + git clone https://github.com/YOUR_USERNAME/api_looter.git + cd api_looter + ``` + +3. **Create a branch**: + ```bash + git checkout -b add-your-api-name + ``` + +4. **Edit `app/data.py`** - Add your API to the `APIS` list: + ```python + { + "id": 15, # Next available ID + "name": "Your API Name", + "description": "What this API does (1-2 sentences).", + "endpoint": "https://api.example.com/v1/endpoint", + "parameters": [ # Leave empty [] if no parameters + {"name": "query", "label": "Search Query", "type": "text", "required": True} + ], + "why_use": "Why would a developer use this API? (Educational!)", + "how_use": "How/when do developers commonly use this API?", + "category": "Data" # Images, Fun, Data, or Cryptocurrency + }, + ``` + +5. **Validate your changes**: + ```bash + python validate_apis.py + ``` + This checks: + - āœ… HTTPS endpoint (not HTTP) + - āœ… No localhost/private IPs + - āœ… All required fields present + - āœ… Educational fields filled in + - āœ… No security issues + +6. **Test locally**: + ```bash + cp .env.development .env + python run.py + ``` + - Visit http://localhost:8000 + - Find your API in the list + - Click on it and test it works! + +7. **Commit and push**: + ```bash + git add app/data.py + git commit -m "Add Your API Name" + git push origin add-your-api-name + ``` + +8. **Open a Pull Request** on GitHub + +That's it! The domain whitelist auto-updates from your endpoint. ✨ + +--- + +## API Requirements + +### āœ… Required +- **Free to use** (no paid subscription) +- **Publicly accessible** (no special access needed) +- **HTTPS endpoint** (not HTTP, except documented legacy APIs) +- **Family-friendly** (no adult/offensive content) +- **Educational value** (helps people learn about APIs) +- **Working** (you tested it!) + +### āŒ Not Accepted +- Localhost/private IP endpoints +- APIs requiring payment +- APIs with adult/offensive content +- Broken/deprecated APIs +- APIs without documentation + +--- + +## Parameter Types + +### Text Input +```python +{"name": "search", "label": "Search Query", "type": "text", "required": True} +``` + +### Dropdown/Select +```python +{ + "name": "category", + "label": "Category", + "type": "select", + "required": True, + "options": [ + {"value": "option1", "label": "Option 1"}, + {"value": "option2", "label": "Option 2"} + ] +} +``` + +--- + +## Custom Handlers (Advanced - Rarely Needed) + +**95% of APIs don't need this!** The default handler works for most APIs. + +Only needed if your API has a very unusual response format. + +See `app/api_helpers.py` for examples, then update `app/routes.py`. + +--- + +## Other Contributions + +### Bug Fixes +1. Create an issue describing the bug +2. Fork and create a branch: `fix-issue-123` +3. Fix the bug +4. Add a test if possible +5. Submit a PR + +### Documentation +1. Fork the repository +2. Update documentation files +3. Submit a PR + +### Security Issues +**DO NOT create public issues!** +See [SECURITY.md](SECURITY.md) for reporting process. + +--- + +## Development Setup + +### Local Development +```bash +# Clone +git clone https://github.com/ComputerAnything/api_looter.git +cd api_looter + +# Setup environment +cp .env.development .env + +# Install dependencies (optional, use virtual env) +python -m venv venv +source venv/bin/activate # Windows: venv\Scripts\activate +pip install -r requirements.txt + +# Run +python run.py +# Visit: http://localhost:8000 +``` + +### Code Quality +```bash +# Lint code +ruff check . + +# Validate APIs +python validate_apis.py + +# Format code +ruff format . +``` + +--- + +## Pull Request Process + +1. **Automated Checks**: Your PR will be automatically checked for: + - API validation (security, format, etc.) + - Code linting + - Security scanning + - Secret detection + +2. **Review**: Maintainers will review your contribution + +3. **Merge**: Once approved and checks pass, we'll merge! + +4. **Recognition**: You'll be credited in the release notes ✨ + +--- + +## Style Guidelines + +- **Python**: Follow PEP 8 (enforced by ruff) +- **Comments**: Explain *why*, not *what* +- **Commit messages**: Clear and descriptive + - Good: "Add Cat Facts API with description" + - Bad: "update" + +--- + +## Questions? + +- **General questions**: Open a GitHub Discussion +- **Bug reports**: Open an issue with the bug report template +- **Feature ideas**: Open an issue with the feature request template +- **Security**: See [SECURITY.md](SECURITY.md) + +--- + +## Code of Conduct + +Be respectful, inclusive, and helpful. See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). + +--- + +## License + +By contributing, you agree your contributions will be licensed under the MIT License. + +--- + +Thank you for making api_looter better! šŸš€ diff --git a/docs/security/SECURITY.md b/docs/security/SECURITY.md new file mode 100644 index 0000000..7e0017a --- /dev/null +++ b/docs/security/SECURITY.md @@ -0,0 +1,137 @@ +# Security Policy + +## Supported Versions + +We actively maintain security updates for the latest version: + +| Version | Supported | +| ------- | ------------------ | +| latest | :white_check_mark: | + +## Security Features + +api_looter implements multiple layers of security: + +### Application Security +- āœ… **SSRF Protection**: Auto-whitelisted domains only (extracted from `data.py`) +- āœ… **Rate Limiting**: 30 requests/minute per IP on API testing endpoints +- āœ… **Input Validation**: 500 character limit on all parameters +- āœ… **Security Headers**: CSP, HSTS, X-Frame-Options, X-Content-Type-Options +- āœ… **HTTPS Enforcement**: Production mode requires HTTPS +- āœ… **No XSS**: React-style template escaping + CSP headers +- āœ… **CORS Configuration**: Proper credential handling + +### Infrastructure Security (Production) +- āœ… **Read-only filesystem**: Containers run with immutable root +- āœ… **Capability dropping**: Minimal Linux capabilities +- āœ… **Non-root user**: Containers don't run as root +- āœ… **Health checks**: Automatic restart on failures +- āœ… **Redis auth**: Password-protected rate limit storage +- āœ… **Network isolation**: Separate Docker networks + +### Dependency Security +- āœ… **Dependabot**: Weekly automated dependency updates +- āœ… **CodeQL**: Automated code scanning +- āœ… **Bandit**: Python security linting +- āœ… **TruffleHog**: Secret scanning + +## Reporting a Vulnerability + +**Please DO NOT create public GitHub issues for security vulnerabilities.** + +Instead, please report security issues privately: + +1. **Email**: [Create a private security advisory on GitHub](https://github.com/ComputerAnything/api_looter/security/advisories/new) +2. **Include**: + - Description of the vulnerability + - Steps to reproduce + - Potential impact + - Suggested fix (if you have one) + +### What to Expect + +- **Response time**: Within 48 hours +- **Updates**: Every 72 hours until resolved +- **Credit**: Security researchers will be credited (unless you prefer to remain anonymous) +- **Fix timeline**: Critical issues within 7 days, high within 14 days + +## Security Best Practices for Contributors + +When adding new APIs to `app/data.py`: + +### āœ… Required Security Checks + +1. **HTTPS Only**: Endpoints MUST use `https://` (not `http://`) + - Exception: Legacy APIs like numbersapi.com (documented) + +2. **No Private IPs**: Do NOT add: + - `localhost`, `127.0.0.1`, `0.0.0.0` + - Private IPs: `10.x.x.x`, `172.16.x.x`-`172.31.x.x`, `192.168.x.x` + - Internal domains: `.internal`, `.corp`, `.lan`, `.local` + +3. **Validate APIs**: Always run before committing: + ```bash + python validate_apis.py + ``` + +4. **Test Locally**: Verify the API works: + ```bash + python run.py + # Visit http://localhost:8000 and test your API + ``` + +### āŒ Security Anti-Patterns + +**Never add APIs that:** +- Require credentials in the URL +- Expose sensitive data (PII, credentials, secrets) +- Are known to have security vulnerabilities +- Return executable code (JavaScript, shell scripts) +- Allow arbitrary URL injection +- Can be used for DDoS amplification + +### šŸ” Example: Secure API Entry + +```python +{ + "id": 15, + "name": "Safe Example API", + "description": "Returns public data only.", + "endpoint": "https://api.example.com/v1/public", # āœ… HTTPS + "parameters": [], + "why_use": "Learn about public APIs.", + "how_use": "Used for testing and education.", + "category": "Data" +} +``` + +## Automated Security Checks + +Every pull request is automatically scanned for: + +- āœ… **API Validation**: `validate_apis.py` checks all security requirements +- āœ… **Code Quality**: Ruff linting +- āœ… **Security Scanning**: Bandit for Python security issues +- āœ… **Secret Detection**: TruffleHog scans for leaked credentials +- āœ… **Dependency Scan**: Snyk checks for vulnerable dependencies + +PRs must pass all checks before merging. + +## Security Update Process + +1. **Discovery**: Vulnerability identified (internal or external report) +2. **Assessment**: Severity and impact evaluated +3. **Fix**: Patch developed and tested +4. **Release**: Security update published +5. **Disclosure**: Public disclosure after fix is deployed +6. **Credit**: Reporter credited in release notes + +## Questions? + +For security-related questions that are NOT vulnerabilities: +- Open a GitHub Discussion +- Email: support@computeranything.dev (public inquiries only) + +--- + +**Last Updated**: January 2026 diff --git a/docs/setup/DEVELOPMENT.md b/docs/setup/DEVELOPMENT.md new file mode 100644 index 0000000..c0c295e --- /dev/null +++ b/docs/setup/DEVELOPMENT.md @@ -0,0 +1,575 @@ +# Development Guide + +Complete guide for running API Looter in **development mode** on your local machine. + +## Recommended Development Workflow + +**Development = Local (fastest iteration, instant hot reload)** +- No Docker needed +- Instant file change detection +- Memory-based rate limiting (no Redis) + +**Staging Test = Docker Compose** (production-like environment for final testing before deploy) + +--- + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Environment File Management](#environment-file-management) +- [Quick Start](#quick-start) +- [Initial Setup](#initial-setup) +- [Daily Development Workflow](#daily-development-workflow) +- [Testing with Docker (Staging)](#testing-with-docker-staging) +- [Common Development Tasks](#common-development-tasks) +- [Troubleshooting](#troubleshooting) + +--- + +## Prerequisites + +- **Python 3.11+** installed +- **Docker** (only for staging tests) +- **Git** for version control + +**Note:** No database needed! This app uses static Python data structures. + +--- + +## Environment File Management + +**IMPORTANT:** This project uses environment-specific `.env` files that you copy into the standard `.env` before running. + +### How It Works + +The application **ALWAYS reads the standard `.env` file**. + +The other files are **templates** you maintain and copy from: +- `.env.development` - Development settings (memory-based rate limiting) +- `.env.staging` - Staging settings (Docker + Redis) +- `.env.production` - Production settings (Docker + Redis + Cloudflare) + +### Switching Environments + +**Development (Local):** +```bash +cp .env.development .env +python run.py +``` + +**Staging (Docker):** +```bash +cp .env.staging .env +# Fill in: SECRET_KEY, REDIS_PASSWORD, CLOUDFLARE_TUNNEL_TOKEN +docker-compose -f docker-compose.staging.yml up -d +``` + +**Production (Docker):** +```bash +cp .env.production .env +# Fill in: SECRET_KEY, REDIS_PASSWORD, CLOUDFLARE_TUNNEL_TOKEN +docker-compose -f docker-compose.prod.yml up -d +``` + +### Why This Approach? + +āœ… **One source of truth** - Application always reads `.env` +āœ… **Easy switching** - Just copy the environment you need +āœ… **Safe** - Original templates stay unchanged +āœ… **Clear** - Know exactly which environment you're using + +**Remember:** The standard `.env` file is in `.gitignore` and never committed. + +--- + +## Quick Start + +**TL;DR - Get coding in 5 minutes:** + +```bash +# STEP 1: Clone repository +git clone +cd api_looter + +# STEP 2: Create virtual environment +python -m venv venv +source venv/bin/activate # Windows: venv\Scripts\activate + +# STEP 3: Install dependencies +pip install -r requirements.txt + +# STEP 4: Set up environment +cp .env.development .env + +# STEP 5: Run the app! +python run.py +``` + +**App:** http://localhost:8000 + +**That's it!** No database setup, no migrations, no Redis container for development. + +--- + +## Initial Setup + +### 1. Clone Repository + +```bash +git clone +cd api_looter +``` + +### 2. Create Virtual Environment + +```bash +# Create venv +python -m venv venv + +# Activate it +source venv/bin/activate # Windows: venv\Scripts\activate + +# Install dependencies +pip install -r requirements.txt +``` + +### 3. Configure Environment + +```bash +# Copy development template +cp .env.development .env + +# (Optional) Edit if you want to change SECRET_KEY +nano .env # or code .env +``` + +**Default `.env.development` contents:** + +```bash +# Development Environment +SECRET_KEY=19eb1b96290552d1dc675c84843f85f7da52543e172ae559b5964a6bf1b2f3ff +REDIS_URL=memory:// +FLASK_ENV=development +FLASK_APP=run.py +``` + +**Key points:** +- `REDIS_URL=memory://` - No Redis container needed, rate limiting uses in-memory storage +- `FLASK_ENV=development` - Enables debug mode and hot reload +- `SECRET_KEY` - Default dev key (change for staging/production) + +### 4. Run the Application + +```bash +python run.py +``` + +You should see: + +``` + * Running on http://127.0.0.1:8000 + * Running on http://192.168.x.x:8000 + * Restarting with stat + * Debugger is active! +``` + +**Visit:** http://localhost:8000 + +**File changes auto-reload instantly!** + +--- + +## Daily Development Workflow + +This is what you'll do every day when coding: + +```bash +# 1. Navigate to project +cd /path/to/api_looter + +# 2. Activate virtualenv +source venv/bin/activate + +# 3. Ensure you're using dev environment +cp .env.development .env + +# 4. Run the app +python run.py +``` + +**That's it!** Just one terminal, instant hot reload. + +### Making Changes + +**To add a new API:** + +1. Edit `app/data.py` +2. Add your API to the `APIS` list +3. Save the file +4. Flask auto-reloads instantly! +5. Refresh browser to see changes + +**Example:** + +```python +# In app/data.py +APIS = [ + # ... existing APIs ... + { + "id": 15, + "name": "My New API", + "description": "Description of the API", + "endpoint": "https://api.example.com/endpoint", + "parameters": [], + "why_use": "Why developers use this API", + "how_use": "How developers use this API", + "category": "Fun" + } +] +``` + +**Domain whitelist updates automatically** - no manual changes needed! + +--- + +## Testing with Docker (Staging) + +Before deploying to production, test in a production-like environment. + +**Staging uses:** +- Docker containers (backend + Redis) +- Real Redis for rate limiting +- Production-like network segmentation +- Cloudflare Tunnel (optional) + +### Start Staging + +```bash +# Copy staging environment +cp .env.staging .env + +# Edit .env and fill in: +# - SECRET_KEY (generate: python -c "import secrets; print(secrets.token_hex(32))") +# - REDIS_PASSWORD (use strong password) +# - CLOUDFLARE_TUNNEL_TOKEN (optional for local staging) + +# Start all services +docker-compose -f docker-compose.staging.yml up --build +``` + +**App:** http://localhost:5000 (or via Cloudflare Tunnel if configured) + +### View Logs + +```bash +# All services +docker-compose -f docker-compose.staging.yml logs -f + +# Specific service +docker-compose -f docker-compose.staging.yml logs -f backend +docker-compose -f docker-compose.staging.yml logs -f redis +``` + +### Stop Staging + +```bash +docker-compose -f docker-compose.staging.yml down +``` + +### Switch Back to Development + +```bash +# Stop Docker +docker-compose -f docker-compose.staging.yml down + +# Copy dev environment +cp .env.development .env + +# Run locally +python run.py +``` + +--- + +## Common Development Tasks + +### Update Dependencies + +```bash +# Activate venv +source venv/bin/activate + +# Update all packages +pip install --upgrade -r requirements.txt + +# Or update specific package +pip install --upgrade Flask +``` + +### Check Security Headers (Local) + +```bash +# Run the app +python run.py + +# In another terminal, check headers +curl -I http://localhost:8000 + +# Should see: +# X-Content-Type-Options: nosniff +# X-Frame-Options: DENY +# X-XSS-Protection: 1; mode=block +# Referrer-Policy: strict-origin-when-cross-origin +``` + +### Test Rate Limiting (Local) + +```bash +# Run the app +python run.py + +# In another terminal, spam requests +for i in {1..35}; do curl -X POST http://localhost:8000/api/1 -d "{}"; done + +# After 30 requests, you should see: +# {"error": "Too many requests. Please try again later.", "retry_after": 60} +``` + +### Validate APIs + +```bash +# Run validation script +python validate_apis.py + +# Should output: +# āœ… Validation passed! +# Total APIs: 14 +# All APIs meet security requirements +``` + +### View Allowed Domains + +```bash +# Run Python shell +python + +>>> from app import create_app +>>> app = create_app() +>>> print(app.config['ALLOWED_API_DOMAINS']) +# Should show all auto-extracted domains +``` + +### Clean Cache Files + +```bash +# Remove Python cache +find . -type d -name "__pycache__" -exec rm -rf {} + +find . -type f -name "*.pyc" -delete +``` + +--- + +## Code Quality + +### Python Linting + +This project follows PEP 8 style guidelines. + +```bash +# Install flake8 (optional) +pip install flake8 + +# Check for issues +flake8 app/ + +# Auto-format with black (optional) +pip install black +black app/ +``` + +--- + +## Troubleshooting + +### Port Already in Use + +```bash +# Find what's using port 8000 +lsof -i :8000 + +# Kill the process +kill -9 + +# Or use a different port +FLASK_RUN_PORT=9000 python run.py +``` + +### "ModuleNotFoundError" + +```bash +# Make sure virtualenv is activated +source venv/bin/activate + +# Reinstall dependencies +pip install -r requirements.txt + +# If still failing, recreate venv +deactivate +rm -rf venv +python -m venv venv +source venv/bin/activate +pip install -r requirements.txt +``` + +### Changes Not Reflecting + +```bash +# Make sure FLASK_ENV=development in .env +cat .env | grep FLASK_ENV + +# Should show: FLASK_ENV=development + +# If not, copy dev template again +cp .env.development .env + +# Restart the app +# Ctrl+C to stop, then: +python run.py +``` + +### Template Not Updating + +```bash +# Flask caches templates. Hard reload browser: +# - Chrome/Firefox: Ctrl + Shift + R +# - Mac: Cmd + Shift + R + +# Or disable caching in .env (not recommended): +# TEMPLATES_AUTO_RELOAD=True +``` + +### Rate Limiting Not Working + +```bash +# Check REDIS_URL in .env +cat .env | grep REDIS_URL + +# For development, should be: memory:// +# If you see redis:// URL, you need Redis running + +# Option 1: Switch to memory-based (no Redis needed) +cp .env.development .env +python run.py + +# Option 2: Start Redis with Docker +docker-compose -f docker-compose.staging.yml up redis -d +``` + +### Docker Issues (Staging) + +```bash +# Rebuild containers +docker-compose -f docker-compose.staging.yml build --no-cache + +# Remove all containers and volumes +docker-compose -f docker-compose.staging.yml down -v + +# Start fresh +docker-compose -f docker-compose.staging.yml up --build +``` + +### Check if APIs Still Work + +```bash +# Test a specific API (Dog CEO) +curl https://dog.ceo/api/breeds/image/random + +# Should return JSON with image URL + +# Test all APIs via the app +python run.py +# Visit http://localhost:8000 +# Click on each API and test +``` + +--- + +## Environment Files Summary + +**Your active config:** +- `.env` - Current active environment (gitignored) + +**Template files (copy to .env as needed):** +- `.env.development` - Local development template +- `.env.staging` - Staging deployment template +- `.env.production` - Production deployment template + +**Copy workflow:** +```bash +# For dev +cp .env.development .env +python run.py + +# For staging test +cp .env.staging .env +docker-compose -f docker-compose.staging.yml up -d + +# For production deploy +cp .env.production .env +docker-compose -f docker-compose.prod.yml up -d +``` + +--- + +## Project Structure Explained + +``` +api_looter/ +ā”œā”€ā”€ app/ +│ ā”œā”€ā”€ __init__.py # Flask app factory +│ │ # - Security headers +│ │ # - Rate limiting setup +│ │ # - CSRF configuration +│ │ # - Auto-domain extraction +│ │ +│ ā”œā”€ā”€ data.py # ⭐ EDIT THIS TO ADD APIs! +│ │ # - Static API list +│ │ # - Helper functions +│ │ +│ ā”œā”€ā”€ routes.py # API endpoints +│ │ # - Homepage (GET /) +│ │ # - API detail (GET/POST /api/) +│ │ +│ ā”œā”€ā”€ api_helpers.py # API call handlers +│ │ # - SSRF protection +│ │ # - Request handling +│ │ # - Response parsing +│ │ +│ ā”œā”€ā”€ static/ # CSS, JS, images +│ │ ā”œā”€ā”€ style.css # Main stylesheet +│ │ ā”œā”€ā”€ script.js # JSON syntax highlighting +│ │ └── images/ # Favicons, etc. +│ │ +│ └── templates/ # HTML templates +│ ā”œā”€ā”€ layout.html # Base template +│ ā”œā”€ā”€ index.html # Homepage (API list) +│ └── api_detail.html # API detail page +│ +ā”œā”€ā”€ docs/ # Documentation +ā”œā”€ā”€ .github/workflows/ # CI/CD automation +ā”œā”€ā”€ validate_apis.py # Security validation +ā”œā”€ā”€ requirements.txt # Python dependencies +ā”œā”€ā”€ run.py # App entry point +└── .env # Active environment (gitignored) +``` + +--- + +## Next Steps + +- Make your changes +- Test locally: `python run.py` +- Test in staging: `docker-compose -f docker-compose.staging.yml up` +- Before deploying: See [PRODUCTION.md](./PRODUCTION.md) + +Happy coding! šŸš€ diff --git a/docs/setup/ENVIRONMENTS.md b/docs/setup/ENVIRONMENTS.md new file mode 100644 index 0000000..b97cbb4 --- /dev/null +++ b/docs/setup/ENVIRONMENTS.md @@ -0,0 +1,154 @@ +# Environment Setup Guide + +## Quick Start + +api_looter uses a **single `.env` file** that you copy from environment-specific templates. + +### Switching Environments + +```bash +# For local development (no Docker, hot reload): +cp .env.development .env +python run.py + +# For staging deployment: +cp .env.staging .env +# Fill in: SECRET_KEY, REDIS_PASSWORD, CLOUDFLARE_TUNNEL_TOKEN +docker-compose -f docker-compose.staging.yml up -d + +# For production deployment: +cp .env.production .env +# Fill in: SECRET_KEY, REDIS_PASSWORD, CLOUDFLARE_TUNNEL_TOKEN +docker-compose -f docker-compose.prod.yml up -d +``` + +--- + +## Environment Details + +### Development (Local, No Docker) +**File:** `.env.development` +**Use Case:** Local development with instant hot reload + +**Features:** +- āœ… No Docker containers needed +- āœ… Hot reload on file changes +- āœ… Memory-based rate limiting (no Redis) +- āœ… Debug mode enabled + +**Run:** +```bash +cp .env.development .env +python run.py +# Visit: http://localhost:8000 +``` + +--- + +### Staging (Docker + Redis) +**File:** `.env.staging` +**Use Case:** Testing before production + +**Features:** +- 🐳 Docker containers (backend + Redis + Cloudflare Tunnel) +- šŸ”’ Real Redis for rate limiting +- 🌐 Cloudflare Tunnel (staging subdomain) + +**Setup:** +```bash +cp .env.staging .env +# Edit .env and fill in: +# - SECRET_KEY (generate: python -c "import secrets; print(secrets.token_hex(32))") +# - REDIS_PASSWORD (use strong password) +# - CLOUDFLARE_TUNNEL_TOKEN (from Cloudflare dashboard) + +docker-compose -f docker-compose.staging.yml up -d +``` + +--- + +### Production (Docker + Redis) +**File:** `.env.production` +**Use Case:** Live production deployment + +**Domain:** Your production domain + +**Features:** +- 🐳 Docker containers with health checks +- šŸ”’ Redis with persistence +- 🌐 Cloudflare Tunnel +- šŸ›”ļø Security hardening (read-only filesystem, capability dropping) + +**Setup:** +```bash +cp .env.production .env +# Edit .env and fill in: +# - SECRET_KEY (NEW random key, different from staging) +# - REDIS_PASSWORD (strong password) +# - CLOUDFLARE_TUNNEL_TOKEN (production tunnel) + +docker-compose -f docker-compose.prod.yml up -d +``` + +--- + +## Environment Variables + +| Variable | Development | Staging | Production | +|----------|-------------|---------|------------| +| `SECRET_KEY` | Any value | Random 64-char hex | Random 64-char hex (different from staging) | +| `REDIS_URL` | `memory://` | `redis://:password@redis:6379/0` | `redis://:password@redis:6379/0` | +| `REDIS_PASSWORD` | N/A | Strong password | Strong password | +| `CLOUDFLARE_TUNNEL_TOKEN` | N/A | Staging tunnel token | Production tunnel token | +| `FLASK_ENV` | `development` | `staging` | `production` | + +--- + +## Important Notes + +- **Never commit `.env` to git** - it's in `.gitignore` +- Use **different SECRET_KEY values** for each environment +- Use **different REDIS_PASSWORD values** for each environment +- Keep production credentials **completely separate** from staging +- The `.env.*` template files are gitignored - fill them in locally + +--- + +## Deployment Workflow + +**Development → Staging → Production** + +1. **Develop locally:** + ```bash + cp .env.development .env + python run.py + ``` + +2. **Test in staging:** + ```bash + cp .env.staging .env + # Fill in staging credentials + docker-compose -f docker-compose.staging.yml up -d + ``` + +3. **Deploy to production:** + ```bash + cp .env.production .env + # Fill in production credentials + docker-compose -f docker-compose.prod.yml up -d + ``` + +--- + +## Stopping Services + +```bash +# Stop development server: +# Press Ctrl+C in terminal + +# Stop staging: +docker-compose -f docker-compose.staging.yml down + +# Stop production: +docker-compose -f docker-compose.prod.yml down +``` diff --git a/migrations/README b/migrations/README deleted file mode 100755 index 0e04844..0000000 --- a/migrations/README +++ /dev/null @@ -1 +0,0 @@ -Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini deleted file mode 100755 index ec9d45c..0000000 --- a/migrations/alembic.ini +++ /dev/null @@ -1,50 +0,0 @@ -# A generic, single database configuration. - -[alembic] -# template used to generate migration files -# file_template = %%(rev)s_%%(slug)s - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic,flask_migrate - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[logger_flask_migrate] -level = INFO -handlers = -qualname = flask_migrate - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py deleted file mode 100755 index 4c97092..0000000 --- a/migrations/env.py +++ /dev/null @@ -1,113 +0,0 @@ -import logging -from logging.config import fileConfig - -from flask import current_app - -from alembic import context - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -fileConfig(config.config_file_name) -logger = logging.getLogger('alembic.env') - - -def get_engine(): - try: - # this works with Flask-SQLAlchemy<3 and Alchemical - return current_app.extensions['migrate'].db.get_engine() - except (TypeError, AttributeError): - # this works with Flask-SQLAlchemy>=3 - return current_app.extensions['migrate'].db.engine - - -def get_engine_url(): - try: - return get_engine().url.render_as_string(hide_password=False).replace( - '%', '%%') - except AttributeError: - return str(get_engine().url).replace('%', '%%') - - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -config.set_main_option('sqlalchemy.url', get_engine_url()) -target_db = current_app.extensions['migrate'].db - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def get_metadata(): - if hasattr(target_db, 'metadatas'): - return target_db.metadatas[None] - return target_db.metadata - - -def run_migrations_offline(): - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, target_metadata=get_metadata(), literal_binds=True - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - - # this callback is used to prevent an auto-migration from being generated - # when there are no changes to the schema - # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html - def process_revision_directives(context, revision, directives): - if getattr(config.cmd_opts, 'autogenerate', False): - script = directives[0] - if script.upgrade_ops.is_empty(): - directives[:] = [] - logger.info('No changes in schema detected.') - - conf_args = current_app.extensions['migrate'].configure_args - if conf_args.get("process_revision_directives") is None: - conf_args["process_revision_directives"] = process_revision_directives - - connectable = get_engine() - - with connectable.connect() as connection: - context.configure( - connection=connection, - target_metadata=get_metadata(), - **conf_args - ) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako deleted file mode 100755 index 2c01563..0000000 --- a/migrations/script.py.mako +++ /dev/null @@ -1,24 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} - - -def upgrade(): - ${upgrades if upgrades else "pass"} - - -def downgrade(): - ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/3c3561ca9eff_initial_migration.py b/migrations/versions/3c3561ca9eff_initial_migration.py deleted file mode 100755 index 8f43a53..0000000 --- a/migrations/versions/3c3561ca9eff_initial_migration.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Initial migration - -Revision ID: 3c3561ca9eff -Revises: -Create Date: 2025-04-26 18:40:54.456495 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '3c3561ca9eff' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('api_model', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=100), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('endpoint', sa.String(length=200), nullable=False), - sa.Column('parameters', sa.JSON(), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('api_model') - # ### end Alembic commands ### diff --git a/migrations/versions/78ed842c38ba_add_api_key_to_apimodel.py b/migrations/versions/78ed842c38ba_add_api_key_to_apimodel.py deleted file mode 100755 index 82afacf..0000000 --- a/migrations/versions/78ed842c38ba_add_api_key_to_apimodel.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Add api_key to APIModel - -Revision ID: 78ed842c38ba -Revises: 3c3561ca9eff -Create Date: 2025-04-26 20:56:34.690598 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '78ed842c38ba' -down_revision = '3c3561ca9eff' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('api_model', schema=None) as batch_op: - batch_op.add_column(sa.Column('api_key', sa.String(length=255), nullable=True)) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('api_model', schema=None) as batch_op: - batch_op.drop_column('api_key') - - # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt index 5df6291..138eb8c 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ -Flask>=2.2.5 -Flask-WTF==1.0.0 -requests==2.26.0 -gunicorn==20.1.0 -Flask-SQLAlchemy -Flask-Migrate -python-dotenv -psycopg2-binary +Flask==2.3.3 +Flask-WTF==1.2.1 +Flask-Limiter==3.5.0 +Flask-CORS==4.0.2 +requests==2.31.0 +gunicorn==21.2.0 +python-dotenv==1.0.0 +redis==5.0.1 diff --git a/run.py b/run.py index 0a23b5a..da09d5c 100755 --- a/run.py +++ b/run.py @@ -1,3 +1,16 @@ +import os from app import create_app app = create_app() + +if __name__ == '__main__': + # Development mode: hot reload enabled + # Use debug=True for auto-reload on file changes + port = int(os.environ.get('PORT', 8000)) + debug = os.environ.get('FLASK_ENV') == 'development' + + app.run( + host='0.0.0.0', + port=port, + debug=debug + ) diff --git a/validate_apis.py b/validate_apis.py new file mode 100755 index 0000000..3c7e07e --- /dev/null +++ b/validate_apis.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +""" +API Validation Script - Enforces security and quality standards +Run this before committing changes to data.py +""" + +import sys +import re +from urllib.parse import urlparse +from typing import List, Tuple +import ipaddress + + +def validate_apis(): + """Validate all APIs in data.py for security and quality""" + # Import the APIS list + sys.path.insert(0, '.') + from app.data import APIS + + errors = [] + warnings = [] + + print("šŸ” Validating APIs...") + print(f"Found {len(APIS)} APIs to validate\n") + + seen_ids = set() + seen_names = set() + seen_endpoints = set() + + for idx, api in enumerate(APIS): + api_num = idx + 1 + api_name = api.get('name', f'API #{api_num}') + + # 1. SECURITY: Check required fields + required_fields = ['id', 'name', 'description', 'endpoint', 'parameters', 'why_use', 'how_use', 'category'] + for field in required_fields: + if field not in api: + errors.append(f"āŒ {api_name}: Missing required field '{field}'") + + # 2. SECURITY: Validate endpoint URL + endpoint = api.get('endpoint', '') + if not endpoint: + errors.append(f"āŒ {api_name}: Endpoint cannot be empty") + continue + + try: + parsed = urlparse(endpoint) + + # SECURITY: Must use HTTPS (not HTTP) + if parsed.scheme != 'https': + # Allow http only for numbersapi.com (legacy API) + if 'numbersapi.com' not in parsed.netloc: + errors.append(f"āŒ {api_name}: Endpoint must use HTTPS, not {parsed.scheme}://") + else: + warnings.append(f"āš ļø {api_name}: Using HTTP (legacy API - consider finding HTTPS alternative)") + + # SECURITY: Check for valid domain + if not parsed.netloc: + errors.append(f"āŒ {api_name}: Invalid endpoint URL (no domain)") + continue + + # SECURITY: Block localhost, private IPs, and internal networks + domain = parsed.netloc.split(':')[0] # Remove port if present + + # Check if it's an IP address + try: + ip = ipaddress.ip_address(domain) + if ip.is_private or ip.is_loopback or ip.is_link_local: + errors.append(f"āŒ {api_name}: Cannot use private/localhost IP addresses: {domain}") + except ValueError: + # Not an IP, it's a domain - that's good + pass + + # SECURITY: Block localhost domains + localhost_patterns = ['localhost', '127.0.0.1', '0.0.0.0', '::1', 'local.', '.local'] + if any(pattern in domain.lower() for pattern in localhost_patterns): + errors.append(f"āŒ {api_name}: Cannot use localhost/local domains: {domain}") + + # SECURITY: Block internal domains + internal_patterns = ['.internal', '.corp', '.lan', '.home'] + if any(domain.lower().endswith(pattern) for pattern in internal_patterns): + errors.append(f"āŒ {api_name}: Cannot use internal domains: {domain}") + + except Exception as e: + errors.append(f"āŒ {api_name}: Invalid endpoint URL: {e}") + + # 3. Check for duplicate IDs + api_id = api.get('id') + if api_id in seen_ids: + errors.append(f"āŒ {api_name}: Duplicate ID {api_id}") + seen_ids.add(api_id) + + # 4. Check for duplicate names + if api_name in seen_names: + errors.append(f"āŒ Duplicate API name: {api_name}") + seen_names.add(api_name) + + # 5. Check for duplicate endpoints + if endpoint in seen_endpoints: + warnings.append(f"āš ļø {api_name}: Duplicate endpoint (may be intentional): {endpoint}") + seen_endpoints.add(endpoint) + + # 6. Validate ID sequence + expected_id = idx + 1 + if api_id != expected_id: + warnings.append(f"āš ļø {api_name}: ID is {api_id}, expected {expected_id} (should be sequential)") + + # 7. Check educational fields are filled + if not api.get('why_use') or len(api.get('why_use', '').strip()) < 10: + errors.append(f"āŒ {api_name}: 'why_use' field is too short (min 10 characters)") + + if not api.get('how_use') or len(api.get('how_use', '').strip()) < 10: + errors.append(f"āŒ {api_name}: 'how_use' field is too short (min 10 characters)") + + # 8. Validate category + valid_categories = ['Images', 'Fun', 'Data', 'Cryptocurrency'] + category = api.get('category', '') + if category and category not in valid_categories: + warnings.append(f"āš ļø {api_name}: New category '{category}' (valid: {', '.join(valid_categories)})") + + # 9. Validate parameters structure + params = api.get('parameters', []) + if not isinstance(params, list): + errors.append(f"āŒ {api_name}: 'parameters' must be a list") + else: + for pidx, param in enumerate(params): + if not isinstance(param, dict): + errors.append(f"āŒ {api_name}: Parameter #{pidx+1} must be a dictionary") + continue + + # Check required parameter fields + param_required = ['name', 'label', 'type', 'required'] + for field in param_required: + if field not in param: + errors.append(f"āŒ {api_name}: Parameter '{param.get('name', pidx+1)}' missing field '{field}'") + + # Validate parameter types + param_type = param.get('type', '') + if param_type not in ['text', 'select']: + errors.append(f"āŒ {api_name}: Parameter '{param.get('name')}' has invalid type '{param_type}' (must be 'text' or 'select')") + + # If select, must have options + if param_type == 'select': + if 'options' not in param or not param['options']: + errors.append(f"āŒ {api_name}: Select parameter '{param.get('name')}' must have 'options'") + + # 10. SECURITY: Check for suspicious content in descriptions + suspicious_keywords = [' Date: Thu, 8 Jan 2026 12:07:43 -0500 Subject: [PATCH 3/7] Add adult content warning modal and update API data structure --- app/data.py | 4 +- app/static/style.css | 223 ++++++++++++++++++++++++++++++---- app/templates/api_detail.html | 27 ++++ 3 files changed, 226 insertions(+), 28 deletions(-) diff --git a/app/data.py b/app/data.py index 5d9f1b9..b432b06 100644 --- a/app/data.py +++ b/app/data.py @@ -45,7 +45,9 @@ "parameters": [], "why_use": "Simple API perfect for practicing JSON data extraction and response handling.", "how_use": "Returns motivational advice - great for learning apps, bots, or daily inspiration features.", - "category": "Fun" + "category": "Fun", + "is_adult": True, + "adult_warning": "This API may contain advice with adult language or mature themes." }, { "id": 5, diff --git a/app/static/style.css b/app/static/style.css index 097493f..754084d 100755 --- a/app/static/style.css +++ b/app/static/style.css @@ -4,6 +4,7 @@ margin-left: 0; list-style: none; } + body::before { content: ""; position: fixed; @@ -17,6 +18,7 @@ body::before { z-index: 0; pointer-events: none; } + :root { --main-bg: #23272a; --panel-bg: #2d333b; @@ -37,7 +39,8 @@ body { header.header-with-logos { - background: #ffffff; /* Mint green */ + background: #ffffff; + /* Mint green */ color: var(--text-main); padding: 0.8em 0 0.6em 0; text-align: center; @@ -62,6 +65,7 @@ header.header-with-logos { .header-logo-left { margin-right: 18px; } + .header-logo-right { margin-left: 18px; } @@ -117,8 +121,10 @@ main { box-shadow: 0 4px 16px rgba(26, 188, 156, 0.10); } -.api-detail-center, .container { - background: rgba(20, 24, 28, 0.98); /* Darker, nearly opaque */ +.api-detail-center, +.container { + background: rgba(20, 24, 28, 0.98); + /* Darker, nearly opaque */ border-radius: 12px; box-shadow: 0 2px 16px rgba(20, 92, 74, 0.18); border: 1.5px solid rgba(26, 188, 156, 0.22); @@ -130,12 +136,14 @@ main { align-items: center; } -.api-detail-center h2, .container h1 { +.api-detail-center h2, +.container h1 { color: var(--accent-green); margin-bottom: 0.5em; } -.api-detail-center p, .container p { +.api-detail-center p, +.container p { color: var(--text-muted); } @@ -155,7 +163,8 @@ label { color: var(--text-main); } -input, select { +input, +select { background: #23272a; color: var(--text-main); border: 1px solid var(--border); @@ -169,12 +178,14 @@ input, select { font-size: 1em; } -input:focus, select:focus { +input:focus, +select:focus { border-color: var(--accent-green); outline: none; } -button, .back-btn { +button, +.back-btn { background: var(--accent-green); color: #fff; border: none; @@ -191,12 +202,16 @@ button, .back-btn { text-align: center; } -button:hover, .back-btn:hover { +button:hover, +.back-btn:hover { background: var(--accent-green-dark); color: #fff; } -.joke-result, pre, .api-detail-center .joke-result, .api-detail-center pre { +.joke-result, +pre, +.api-detail-center .joke-result, +.api-detail-center pre { background: #23272a; color: var(--text-main); border-radius: 8px; @@ -217,31 +232,33 @@ button:hover, .back-btn:hover { } .footer-logo { - width: 45px; - height: 45px; - border-radius: 5px; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); + width: 45px; + height: 45px; + border-radius: 5px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); } .footer-powered-by { - display: inline-flex; - align-items: center; - gap: 0.5rem; - white-space: nowrap; - min-width: 0; + display: inline-flex; + align-items: center; + gap: 0.5rem; + white-space: nowrap; + min-width: 0; } .mx-1 { - margin-left: 0.25rem; - margin-right: 0.25rem; + margin-left: 0.25rem; + margin-right: 0.25rem; } + .d-none { - display: none !important; + display: none !important; } + @media (min-width: 576px) { - .d-sm-inline { - display: inline !important; - } + .d-sm-inline { + display: inline !important; + } } /* Educational Content Section */ @@ -272,15 +289,167 @@ button:hover, .back-btn:hover { } @media (max-width: 600px) { - .api-detail-center, .container { + + .api-detail-center, + .container { max-width: 98vw; padding: 1em 0.3em; } - input, select, .joke-result, pre { + + input, + select, + .joke-result, + pre { max-width: 98vw; } + .api-education { padding: 1rem; margin-bottom: 1.5rem; } } + +/* Adult Content Warning Modal */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + animation: fadeIn 0.3s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +.modal-content { + background: var(--panel-bg); + border: 2px solid var(--accent-green); + border-radius: 12px; + padding: 2rem; + margin: auto; + margin-top: 10vh; + max-width: 450px; + width: 90%; + text-align: center; + box-shadow: 0 4px 20px rgba(26, 188, 156, 0.25); + animation: slideDown 0.3s ease-out; +} + +@keyframes slideDown { + from { + transform: translateY(-50px); + opacity: 0; + } + + to { + transform: translateY(0); + opacity: 1; + } +} + +.modal-content h2 { + color: var(--accent-green); + margin-top: 0; + margin-bottom: 1rem; + font-size: 1.5em; +} + +.modal-content p { + color: var(--text-muted); + margin-bottom: 1.5rem; + line-height: 1.6; + font-size: 1.05em; +} + +.modal-buttons { + display: flex; + /* gap: 1rem; */ + justify-content: center; + flex-wrap: wrap; + flex-direction: column; +} + +.modal-btn { + padding: 0.7em 1.8em; + border: none; + border-radius: 6px; + font-size: 1em; + font-weight: bold; + cursor: pointer; + transition: all 0.2s; + text-decoration: none; +} + +.modal-btn-confirm { + background: var(--accent-green); + color: #fff; +} + +.modal-btn-confirm:hover { + background: var(--accent-green-dark); + box-shadow: 0 4px 12px rgba(26, 188, 156, 0.3); +} + +.modal-btn-back { + background: transparent; + color: var(--text-muted); + border: 1px solid var(--border); +} + +.modal-btn-back:hover { + background: rgba(26, 188, 156, 0.1); + color: var(--accent-green); + border-color: var(--accent-green); +} + +/* Mobile Modal Styles */ +@media (max-width: 600px) { + .modal-content { + margin-top: 15vh; + max-width: 75vw; + width: 95vw; + padding: 1.5rem 1rem; + border-radius: 10px; + } + + .modal-content h2 { + font-size: 1.3em; + margin-bottom: 0.8rem; + } + + .modal-content p { + font-size: 0.95em; + margin-bottom: 1.2rem; + } + + .modal-buttons { + flex-direction: column; + /* gap: 0.75rem; */ + } + + .modal-btn { + padding: 0.8em 1.5em; + font-size: 0.95em; + width: 100%; + box-sizing: border-box; + } + + .modal-btn-confirm { + order: 1; + } + + .modal-btn-back { + order: 2; + } +} diff --git a/app/templates/api_detail.html b/app/templates/api_detail.html index c885de8..c220146 100755 --- a/app/templates/api_detail.html +++ b/app/templates/api_detail.html @@ -1,6 +1,21 @@ {% extends "layout.html" %} {% block title %}API Looter - {{ api.name }}{% endblock %} {% block content %} + + +{% if api.get('is_adult') %} + +{% endif %} +

    {{ api.name }}

    {{ api.description }}

    @@ -84,6 +99,18 @@

    Result:

    +{% endif %} - // Add a loading spinner when the form is submitted + +