From 04088cc631cf95b3ab910cd607a2f61fe1fb7e75 Mon Sep 17 00:00:00 2001 From: Tiago Correia Date: Fri, 12 Jun 2026 11:00:46 +0100 Subject: [PATCH] feat(dev): support running locally without full auth/S3 config Make the app boot gracefully when optional integrations are absent, and document the full local setup: - OIDC: skip Keycloak client registration when OIDC_ISSUER_URL is unset and log a warning, so `flask db ...` and other CLI commands work without auth configured (auth routes fail only on actual login). - storage: public_url() returns "" when S3_PUBLIC_BASE_URL is unset, so pages render with broken tags instead of 500ing. - compose: comment out the local Keycloak service/volume (local dev points at the remote realm); keep Postgres + MinIO. - .env.example: add OIDC and S3/MinIO dev defaults. - README: document the OIDC/S3 env vars, MinIO bootstrap, and switch the schema step from `flask db init/migrate` to `flask db upgrade` against the committed migrations. Co-Authored-By: Claude Opus 4.8 (1M context) --- .env.example | 12 ++++++++++ README.md | 57 +++++++++++++++++++++++++++++++++------------- app/__init__.py | 39 +++++++++++++++++++------------ app/storage.py | 9 ++++++-- docker-compose.yml | 36 ++++++++++++++--------------- 5 files changed, 103 insertions(+), 50 deletions(-) diff --git a/.env.example b/.env.example index a7adb0f..4191c72 100644 --- a/.env.example +++ b/.env.example @@ -2,3 +2,15 @@ FLASK_APP=wsgi.py FLASK_DEBUG=1 SECRET_KEY=change-me-in-production DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5521/tkc_library + +# auth +OIDC_ISSUER_URL=https://login.keyholding.com/realms/keyholding +OIDC_CLIENT_ID=tkc-library +OIDC_CLIENT_SECRET=dev-secret + +# s3 +S3_BUCKET=tkc-librarian-uploads-services +S3_ENDPOINT_URL=http://localhost:9000 +S3_PUBLIC_BASE_URL=http://localhost:9000/tkc-librarian-uploads-services +AWS_ACCESS_KEY_ID=minioadmin +AWS_SECRET_ACCESS_KEY=minioadmin \ No newline at end of file diff --git a/README.md b/README.md index 1c2ff3a..7e4182d 100644 --- a/README.md +++ b/README.md @@ -45,37 +45,62 @@ pip install -r requirements.txt cp .env.example .env ``` -The defaults work out-of-the-box with the bundled Docker Compose setup. Edit `.env` only if you change ports, credentials, or want a different `SECRET_KEY`. - -| Variable | Default | Purpose | -| --------------- | ------------------------------------------------------------------------ | ---------------------------------------- | -| `FLASK_APP` | `wsgi.py` | Tells `flask` which app to load | -| `FLASK_DEBUG` | `1` | Enables auto-reload + debugger | -| `SECRET_KEY` | `change-me-in-production` | Session/CSRF signing key | -| `DATABASE_URL` | `postgresql+psycopg://postgres:postgres@localhost:5521/tkc_library` | SQLAlchemy connection string | - -### 4. Start PostgreSQL +The defaults work out-of-the-box with the bundled Docker Compose setup. You **must** set `OIDC_CLIENT_SECRET` (get it from the Keycloak client config) to log in; everything else has working defaults. Edit `.env` if you change ports, credentials, or want a different `SECRET_KEY`. + +| Variable | Default | Purpose | +| ----------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| `FLASK_APP` | `wsgi.py` | Tells `flask` which app to load | +| `FLASK_DEBUG` | `1` | Enables auto-reload + debugger | +| `SECRET_KEY` | `change-me-in-production` | Session/CSRF signing key | +| `DATABASE_URL` | `postgresql+psycopg://postgres:postgres@localhost:5521/tkc_library` | SQLAlchemy connection string | +| `OIDC_ISSUER_URL` | `https://login.keyholding.com/realms/keyholding` | Keycloak realm root; Authlib appends `/.well-known/openid-configuration` | +| `OIDC_CLIENT_ID` | `tkc-library` | OIDC client ID registered in Keycloak | +| `OIDC_CLIENT_SECRET` | _(none — get from Keycloak)_ | OIDC client secret; **required for login** | +| `S3_BUCKET` | `tkc-librarian-uploads-services` | Bucket for book photos | +| `S3_ENDPOINT_URL` | `http://localhost:9000` | Points boto3 at local MinIO; leave **unset in prod** to use AWS S3 | +| `S3_PUBLIC_BASE_URL` | `http://localhost:9000/tkc-librarian-uploads-services` | Browser-facing base URL for serving images | +| `AWS_ACCESS_KEY_ID` | `minioadmin` | MinIO/S3 access key (needed for uploads) | +| `AWS_SECRET_ACCESS_KEY` | `minioadmin` | MinIO/S3 secret key (needed for uploads) | + +> If `OIDC_ISSUER_URL` or `S3_PUBLIC_BASE_URL` are unset, the app still boots (handy for running `flask db …` commands): auth routes are disabled and book images render broken, rather than crashing the page. + +### 4. Start the backing services ```bash docker compose up -d ``` -This starts Postgres 16 on host port **5521** (container port 5432) with database `tkc_library`. Verify it's healthy: +This starts: + +- **Postgres 16** on host port **5521** (container port 5432), database `tkc_library`. +- **MinIO** (S3-compatible object store) on **9000** (S3 API) and **9001** (web console, login `minioadmin` / `minioadmin`). +- A one-shot `minio-bootstrap` job that creates the `tkc-librarian-uploads-services` bucket with a public-read policy, then exits. + +Verify the long-running services are healthy: ```bash docker compose ps ``` -You should see `STATUS Up ... (healthy)`. +You should see `db` and `minio` as `Up ... (healthy)` (the `minio-bootstrap` job will show `Exited (0)` once the bucket is created). -### 5. Initialize the database schema +### 5. Apply the database schema + +The repo ships with committed migrations under `migrations/versions/`, so a fresh +checkout only needs to **apply** them — do **not** run `flask db init` or +`flask db migrate` (those scaffold the migrations dir and author new migrations +respectively, neither of which you want here): ```bash -flask db init # one-time: creates the migrations/ directory -flask db migrate -m "initial" # autogenerates a migration from the models -flask db upgrade # applies it to the database +flask db upgrade ``` +This brings your empty database up to the current schema and runs the seed +migrations (initial books, admin user). The seed rows reference image files that +live in the production bucket, so locally the book thumbnails will 404 until +those images are uploaded to your MinIO bucket — the pages themselves render +fine. + ### 6. Run the development server ```bash diff --git a/app/__init__.py b/app/__init__.py index eb49ef8..fc97d99 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -48,20 +48,31 @@ def create_app(config_class=Config): # Authlib loads the realm's OIDC discovery doc on first use to pick up # endpoints and JWKS — no need to hard-code per-realm URLs. oauth.init_app(app) - oauth.register( - name="keycloak", - server_metadata_url=f"{app.config['OIDC_ISSUER_URL'].rstrip('/')}/.well-known/openid-configuration", - client_id=app.config["OIDC_CLIENT_ID"], - client_secret=app.config["OIDC_CLIENT_SECRET"], - # PKCE is required by the Keycloak client config (S256). Authlib - # doesn't auto-enable it from server metadata; opting in here makes - # the SDK generate the code_verifier and send code_challenge / - # code_challenge_method on the authorize redirect. - client_kwargs={ - "scope": "openid email profile", - "code_challenge_method": "S256", - }, - ) + issuer_url = app.config["OIDC_ISSUER_URL"] + if issuer_url: + oauth.register( + name="keycloak", + server_metadata_url=f"{issuer_url.rstrip('/')}/.well-known/openid-configuration", + client_id=app.config["OIDC_CLIENT_ID"], + client_secret=app.config["OIDC_CLIENT_SECRET"], + # PKCE is required by the Keycloak client config (S256). Authlib + # doesn't auto-enable it from server metadata; opting in here makes + # the SDK generate the code_verifier and send code_challenge / + # code_challenge_method on the authorize redirect. + client_kwargs={ + "scope": "openid email profile", + "code_challenge_method": "S256", + }, + ) + else: + # No OIDC config — e.g. running `flask db ...` or other CLI commands + # locally without auth set up. Skip client registration so the app + # factory doesn't crash; the /auth/* routes resolve `oauth.keycloak` + # lazily and will fail only if someone actually tries to log in. + app.logger.warning( + "OIDC_ISSUER_URL is unset — Keycloak auth disabled; /auth/* routes " + "will be unavailable until it is configured." + ) from app.auth import bp as auth_bp from app.library import bp as library_bp diff --git a/app/storage.py b/app/storage.py index f4cf761..71baceb 100644 --- a/app/storage.py +++ b/app/storage.py @@ -92,7 +92,12 @@ def public_url(key: str) -> str: """ if not key: return "" + base = current_app.config["S3_PUBLIC_BASE_URL"] + if not base: + # No public bucket configured — e.g. local dev without the S3/MinIO + # env vars set. Return "" so templates render a broken rather + # than 500ing the whole page on a missing optional config. + return "" if "/" not in key: key = f"{UPLOAD_PREFIX}{key}" - base = current_app.config["S3_PUBLIC_BASE_URL"].rstrip("/") - return f"{base}/{key}" + return f"{base.rstrip('/')}/{key}" diff --git a/docker-compose.yml b/docker-compose.yml index e0d89ff..a3fe7ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,25 +55,25 @@ services: echo 'MinIO bucket ready: '$$BUCKET " - # Local Keycloak for OIDC. Realm import file lives in the keycloak-config - # repo and gets dropped into ./keycloak-import/ before `docker compose up` - # (see README). Dev-mode start so HTTPS isn't required on localhost. - keycloak: - image: quay.io/keycloak/keycloak:25.0 - restart: unless-stopped - command: start-dev --import-realm - environment: - KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: admin - KC_HOSTNAME_STRICT: "false" - KC_HTTP_ENABLED: "true" - ports: - - "8081:8080" - volumes: - - ./keycloak-import:/opt/keycloak/data/import:ro - - keycloak_data:/opt/keycloak/data + # # Local Keycloak for OIDC. Realm import file lives in the keycloak-config + # # repo and gets dropped into ./keycloak-import/ before `docker compose up` + # # (see README). Dev-mode start so HTTPS isn't required on localhost. + # keycloak: + # image: quay.io/keycloak/keycloak:25.0 + # restart: unless-stopped + # command: start-dev --import-realm + # environment: + # KEYCLOAK_ADMIN: admin + # KEYCLOAK_ADMIN_PASSWORD: admin + # KC_HOSTNAME_STRICT: "false" + # KC_HTTP_ENABLED: "true" + # ports: + # - "8081:8080" + # volumes: + # - ./keycloak-import:/opt/keycloak/data/import:ro + # - keycloak_data:/opt/keycloak/data volumes: postgres_data: minio_data: - keycloak_data: + # keycloak_data: