diff --git a/.env.test b/.env.test deleted file mode 100644 index 9a7ef0f..0000000 --- a/.env.test +++ /dev/null @@ -1,12 +0,0 @@ -TESTING=true -DB_URI=sqlite:///:memory: -S3_ACCESS_KEY=lenny -S3_SECRET_KEY=lennytesting -MINIO_BUCKET=lenny -MINIO_HOST=localhost -MINIO_PORT=9000 -POSTGRES_HOST=localhost -POSTGRES_USER=lenny -POSTGRES_PASSWORD=lennytest -POSTGRES_PORT=5432 -POSTGRES_DB=lending_system \ No newline at end of file diff --git a/README.md b/README.md index 70cedeb..282e6cd 100644 --- a/README.md +++ b/README.md @@ -27,15 +27,25 @@ Lenny is a free, open source Lending System for Libraries. ## Installation -First, copy the file `lenny_TEMPLATE.env` to `lenny.env` (gitignored) and edit it to have the correct values (such as desired psql credentials). +``` +docker/configure.sh # generates lenny.env +docker compose -p lenny up -d --build +``` -Second, run docker compose: +Navigate to localhost:8080 or whatever `$LENNY_PORT` you specified in your `lenny.env` + +You may enter the API container via: ``` -docker compose -p lenny up -d --build +docker exec -it lenny_api bash ``` -Finally, navigate to localhost:8080 or whatever `$LENNY_PORT` you specified in your `lenny.env` +## Rebuilding + +``` +docker compose -p lenny down +docker compose -p lenny up -d --build +``` ## Pilot diff --git a/compose.yaml b/compose.yaml index f84e19d..e2219b7 100644 --- a/compose.yaml +++ b/compose.yaml @@ -12,7 +12,10 @@ services: - s3 env_file: lenny.env environment: - - LENNY_PORT=1337 + - LENNY_PORT=1337 # When in docker, run Lenny as 1337, reverse proxies from nginx + - S3_ENDPOINT=s3:9000 + volumes: + - .:/app networks: - lenny_network diff --git a/docker/Dockerfile b/docker/Dockerfile index 27a6960..d7abb7c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,18 +5,22 @@ WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -# Set only non-sensitive environment variables +# Set non-sensitive environment variables ENV PYTHONUNBUFFERED=1 ENV PYTHONPATH=/app +# Expose ports (FastAPI, PostgreSQL, MinIO API, MinIO Console) EXPOSE 8080 5432 9000 9001 COPY ./lenny/ /app/lenny/ RUN chmod +x /app/lenny/app.py -# Setup NGINX -RUN apt-get update && apt-get install -y nginx +RUN apt-get update && apt-get install -y \ + nginx \ + libpq-dev \ + postgresql-client RUN adduser --disabled-password --gecos "" nginx + COPY ./docker/nginx/nginx.conf /etc/nginx/nginx.conf COPY ./docker/nginx/conf.d/lenny.conf /etc/nginx/conf.d/lenny.conf diff --git a/docker/configure.sh b/docker/configure.sh new file mode 100755 index 0000000..9852b1b --- /dev/null +++ b/docker/configure.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +ENV_FILE="lenny.env" + +# Exit if the file already exists +if [ -f "$ENV_FILE" ]; then + echo "$ENV_FILE already exists. No changes made." + exit 0 +fi + +# Use environment variables if they are set, otherwise provide defaults or generate secure values +LENNY_DOMAIN="${LENNY_DOMAIN:-localhost}" +LENNY_HOST="${LENNY_HOST:-0.0.0.0}" +LENNY_PORT="${LENNY_PORT:-8080}" +LENNY_WORKERS="${LENNY_WORKERS:-1}" +LENNY_LOG_LEVEL="${LENNY_LOG_LEVEL:-debug}" +LENNY_RELOAD="${LENNY_RELOAD:-1}" +LENNY_SSL_CRT="${LENNY_SSL_CRT:-}" +LENNY_SSL_KEY="${LENNY_SSL_KEY:-}" + +DB_USER="${POSTGRES_USER:-librarian}" +DB_HOST="${POSTGRES_HOST:-127.0.0.1}" +DB_PORT="${POSTGRES_PORT:-5432}" +DB_PASSWORD="${POSTGRES_PASSWORD:-$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 32)}" +DB_NAME="${DB_NAME:-lenny}" + +MINIO_ROOT_USER="${MINIO_ROOT_USER:-$(openssl rand -base64 30 | tr -dc 'A-Za-z0-9' | head -c 20)}" +MINIO_ROOT_PASSWORD="${MINIO_ROOT_PASSWORD:-$(openssl rand -base64 60 | tr -dc 'A-Za-z0-9' | head -c 40)}" +MINIO_SECURE="${MINIO_SECURE:-false}" + +# Write to lenny.env +cat < "$ENV_FILE" +# API App (FastAPI) +LENNY_DOMAIN=$LENNY_DOMAIN +LENNY_HOST=$LENNY_HOST +LENNY_PORT=$LENNY_PORT +LENNY_WORKERS=$LENNY_WORKERS +LENNY_LOG_LEVEL=$LENNY_LOG_LEVEL +LENNY_RELOAD=$LENNY_RELOAD +LENNY_SSL_CRT=$LENNY_SSL_CRT +LENNY_SSL_KEY=$LENNY_SSL_KEY + +# DB (PostgreSQL) +DB_USER=$DB_USER +DB_HOST=$DB_HOST +DB_PORT=$DB_PORT +DB_PASSWORD=$DB_PASSWORD +DB_NAME=$DB_NAME + +# MinIO (S3) +MINIO_ROOT_USER=$MINIO_ROOT_USER +MINIO_ROOT_PASSWORD=$MINIO_ROOT_PASSWORD +MINIO_SECURE=$MINIO_SECURE +EOF diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf index e0c47d0..74e2a88 100644 --- a/docker/nginx/nginx.conf +++ b/docker/nginx/nginx.conf @@ -14,6 +14,7 @@ http { access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; + client_max_body_size 50M; include /etc/nginx/conf.d/*.conf; } diff --git a/lenny/app.py b/lenny/app.py index 4d057fd..d10b5e6 100755 --- a/lenny/app.py +++ b/lenny/app.py @@ -1,13 +1,18 @@ #!/usr/bin/env python3 -import uvicorn from fastapi import FastAPI from lenny.routes import api from lenny.configs import OPTIONS +from lenny import __version__ as VERSION -app = FastAPI() +app = FastAPI( + title="Lenny API", + description="Lenny: A Free, Open Source Lending System for Libraries", + version=VERSION, +) app.include_router(api.router, prefix="/v1/api") if __name__ == "__main__": + import uvicorn uvicorn.run("lenny.app:app", **OPTIONS) diff --git a/lenny/configs/__init__.py b/lenny/configs/__init__.py index c25c40c..4d74893 100644 --- a/lenny/configs/__init__.py +++ b/lenny/configs/__init__.py @@ -8,12 +8,10 @@ """ import os -from dotenv import load_dotenv -# Load environment variables from .env.test file -load_dotenv(dotenv_path=".env.test", override= True) -TESTING = os.environ.get("TESTING", "False").lower() == "true" +# Determine environment +TESTING = os.getenv("TESTING", "false").lower() == "true" # API server configuration DOMAIN = os.environ.get('LENNY_DOMAIN', '127.0.0.1') @@ -37,27 +35,26 @@ OPTIONS['ssl_keyfile'] = SSL_KEY OPTIONS['ssl_certfile'] = SSL_CRT +DB_CONFIG = { + 'user': os.environ.get('DB_USER', 'postgres'), + 'password': os.environ.get('DB_PASSWORD'), + 'host': os.environ.get('DB_HOST', 'localhost'), + 'port': int(os.environ.get('DB_PORT', '5432')), + 'dbname': os.environ.get('DB_NAME', 'lenny'), +} + # Database configuration -if TESTING: - DB_URI = "sqlite:///:memory:" -else: - DB_CONFIG = { - 'user': os.environ.get('POSTGRES_USER', 'lenny'), - 'password': os.environ.get('POSTGRES_PASSWORD', 'lennytest'), - 'host': os.environ.get('POSTGRES_HOST', 'postgres'), - 'port': int(os.environ.get('POSTGRES_PORT', '5432')), - 'dbname': os.environ.get('POSTGRES_DB', 'lending_system'), - } - DB_URI = 'postgresql+psycopg2://{user}:{password}@{host}:{port}/{dbname}'.format(**DB_CONFIG) +DB_URI = ( + "sqlite:///:memory:" if TESTING else + 'postgresql+psycopg2://{user}:{password}@{host}:{port}/{dbname}'.format(**DB_CONFIG) +) # MinIO configuration S3_CONFIG = { - 'access_key': os.environ.get('S3_ACCESS_KEY', os.environ.get('MINIO_ROOT_USER')), - 'secret_key': os.environ.get('S3_SECRET_KEY', os.environ.get('MINIO_ROOT_PASSWORD')), - 'endpoint': f"{os.environ.get('MINIO_HOST', 'minio')}:{os.environ.get('MINIO_PORT', '9000')}", - 'secure': False, - 'public_bucket': os.environ.get('MINIO_BUCKET', 'lenny') + "-public", - 'protected_bucket': os.environ.get('MINIO_BUCKET', 'lenny') + "-protected", + 'endpoint': os.environ.get('S3_ENDPOINT'), + 'access_key': os.environ.get('MINIO_ROOT_USER'), + 'secret_key': os.environ.get('MINIO_ROOT_PASSWORD'), + 'secure': os.environ.get('S3_SECURE', 'false').lower() == 'true', } __all__ = ['DOMAIN', 'HOST', 'PORT', 'DEBUG', 'OPTIONS', 'DB_URI', 'DB_CONFIG','S3_CONFIG', 'TESTING'] diff --git a/lenny/models/__init__.py b/lenny/models/__init__.py index c836ae1..8a726ad 100644 --- a/lenny/models/__init__.py +++ b/lenny/models/__init__.py @@ -8,26 +8,42 @@ """ from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker, declarative_base -from lenny.configs import DB_URI, TESTING +from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.ext.declarative import declarative_base +from minio import Minio + +from lenny.configs import DB_URI, S3_CONFIG, DEBUG Base = declarative_base() -if TESTING: - engine = create_engine("sqlite:///:memory:", connect_args={'check_same_thread': False}) - Base.metadata.creat_all(bind=engine) -else: - engine = create_engine(DB_URI) - -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - -def get_db(): - session = SessionLocal() - try: - yield session - finally: - session.close() - if TESTING: - engine.dispose() - -__all__ = ["Base", "SessionLocal", "get_db", "engine"] \ No newline at end of file +# Configure Database Connection +engine = create_engine(DB_URI, echo=DEBUG, client_encoding='utf8') +db = scoped_session(sessionmaker(bind=engine, autocommit=False, autoflush=False)) + +# Configure S3 Connection +s3 = Minio( + endpoint=S3_CONFIG["endpoint"], + access_key=S3_CONFIG["access_key"], + secret_key=S3_CONFIG["secret_key"], + secure=S3_CONFIG["secure"], +) + +# Instantiate s3 buckets +for bucket_name in ["bookshelf-public", "bookshelf-encrypted"]: + if not s3.bucket_exists(bucket_name): + s3.make_bucket(bucket_name) + # Setting public read-only policy + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": ["*"]}, + "Action": ["s3:GetObject"], + "Resource": [f"arn:aws:s3:::{bucket_name}/*"] + } + ] + } + s3.set_bucket_policy(bucket_name, json.dumps(policy)) + +__all__ = ["Base", "db", "s3", "engine"] diff --git a/lenny_TEMPLATE.env b/lenny_TEMPLATE.env deleted file mode 100644 index a34f1b7..0000000 --- a/lenny_TEMPLATE.env +++ /dev/null @@ -1,24 +0,0 @@ - -# Copy this file to `lenny.env` (which is in .gitignore and should not be checked in) - -# FastAPI App -LENNY_DOMAIN= -LENNY_HOST=0.0.0.0 -LENNY_PORT=8080 -LENNY_WORKERS=1 -LENNY_LOG_LEVEL=debug -LENNY_RELOAD=0 -LENNY_DEBUG=1 -LENNY_SSL_CRT= -LENNY_SSL_KEY= - -# PostgreSQL -POSTGRES_USER=lenny -POSTGRES_HOST=127.0.0.1 -POSTGRES_PASSWORD= -POSTGRES_DB=lending_system - -# MinIO (S3) -MINIO_ROOT_USER= -MINIO_ROOT_PASSWORD= -MINIO_BUCKET=lenny diff --git a/requirements.txt b/requirements.txt index 7a77804..159cf88 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,33 @@ +annotated-types==0.7.0 +anyio==4.9.0 +argon2-cffi==23.1.0 +argon2-cffi-bindings==21.2.0 +certifi==2025.4.26 +cffi==1.17.1 +charset-normalizer==3.4.2 +click==8.2.0 +dotenv==0.9.9 fastapi==0.115.4 -uvicorn==0.32.0 -pydantic==2.9.2 -SQLAlchemy==2.0.39 +greenlet==3.2.2 +h11==0.16.0 +idna==3.10 +minio==7.2.9 psycopg2-binary==2.9.10 -pyyaml==6.0.2 +pycparser==2.22 +pycryptodome==3.22.0 +pydantic==2.9.2 +pydantic_core==2.23.4 +python-dotenv==1.1.0 +PyYAML==6.0.2 requests==2.32.3 python-dotenv==1.1.0 typing_extensions==4.12.2 minio==7.2.9 pytest==7.4.2 -psycopg2==2.9.10 \ No newline at end of file +psycopg2==2.9.10 +sniffio==1.3.1 +SQLAlchemy==2.0.39 +starlette==0.41.3 +typing_extensions==4.12.2 +urllib3==2.4.0 +uvicorn==0.32.0