Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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

Comment on lines +6 to +10
# 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
57 changes: 41 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 25 additions & 14 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
)
Comment on lines +51 to +75

from app.auth import bp as auth_bp
from app.library import bp as library_bp
Expand Down
9 changes: 7 additions & 2 deletions app/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <img> rather
# than 500ing the whole page on a missing optional config.
return ""
Comment on lines +95 to +100
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}"
36 changes: 18 additions & 18 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: