Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ef968fd10 | ||
|
|
9fb512cdab | ||
|
|
f0a9728016 |
@@ -3,3 +3,6 @@
|
|||||||
# Extra CA certificates dropped in for building behind a proxy
|
# Extra CA certificates dropped in for building behind a proxy
|
||||||
koha/certs/*.crt
|
koha/certs/*.crt
|
||||||
quickadd/certs/*.crt
|
quickadd/certs/*.crt
|
||||||
|
# Generated by install.sh --domain
|
||||||
|
Caddyfile
|
||||||
|
caddy-root.crt
|
||||||
|
|||||||
+33
-4
@@ -12,9 +12,9 @@ it never overwrites your `.env` (passwords) or your data.
|
|||||||
|
|
||||||
| What | Address | Login |
|
| What | Address | Login |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Your library**: add books, find a book, people, lending, labels, settings | `http://<this machine>:8081` | `admin` and the generated password |
|
| **Your library**: add books, find a book, people, lending, labels, settings | `http://<this machine>:8098` | `admin` and the generated password |
|
||||||
| **Add books** (scan an ISBN, or use the phone camera) | `http://<this machine>:8081/add/` | same login (asked once more) |
|
| **Add books** (scan an ISBN, or use the phone camera) | `http://<this machine>:8098/add/` | same login (asked once more) |
|
||||||
| **Public catalogue** (search only, for a kiosk or visitors) | `http://<this machine>:8080` | none |
|
| **Public catalogue** (search only, for a kiosk or visitors) | `http://<this machine>:8097` | none |
|
||||||
|
|
||||||
For home use you only need the first address: sign in to **Your library**, and use the **Add books**
|
For home use you only need the first address: sign in to **Your library**, and use the **Add books**
|
||||||
button on its home page. The Add books page has a **Find a book** tab (title, author or ISBN, with the
|
button on its home page. The Add books page has a **Find a book** tab (title, author or ISBN, with the
|
||||||
@@ -24,7 +24,7 @@ staff sites the same URL shapes, so they cannot share one address; Add books can
|
|||||||
|
|
||||||
Ports move up to the next free number if one is taken; the final addresses are printed
|
Ports move up to the next free number if one is taken; the final addresses are printed
|
||||||
at the end and by `sudo ./install.sh --status`. Add books is also reachable directly on its own port
|
at the end and by `sudo ./install.sh --status`. Add books is also reachable directly on its own port
|
||||||
(`8082`), which is handy behind a proxy but not needed otherwise.
|
(`8099`), which is handy behind a proxy but not needed otherwise.
|
||||||
|
|
||||||
## What it is made of
|
## What it is made of
|
||||||
|
|
||||||
@@ -210,6 +210,11 @@ Staff who are signed in to the library site (and allowed to edit the catalogue)
|
|||||||
|
|
||||||
## The home page (the Room for Books site at `/add/`)
|
## The home page (the Room for Books site at `/add/`)
|
||||||
|
|
||||||
|
**One address.** `http://<ip>:<port>/` (and later `https://your-domain/` behind Caddy) opens this page; there is no separate
|
||||||
|
port to remember. Koha's own staff screens live on the same address at `/cgi-bin/koha/mainpage.pl`, reached from the menu
|
||||||
|
(*Koha (full staff screens)*). Everyone signs in at the same prompt: staff with the admin login, borrowers with their card number
|
||||||
|
and password, the guest with the guest login.
|
||||||
|
|
||||||
One simple page: a **Look up** box (books by title, author or ISBN, and people by name or card number), four big buttons
|
One simple page: a **Look up** box (books by title, author or ISBN, and people by name or card number), four big buttons
|
||||||
(**Check out**, **Return**, **Add books**, **Add a person**) and **Recently added** books with their covers. The **menu**
|
(**Check out**, **Return**, **Add books**, **Add a person**) and **Recently added** books with their covers. The **menu**
|
||||||
(top right) holds the rest: browse all books, lend / return, people and library cards, barcodes and settings, export / import,
|
(top right) holds the rest: browse all books, lend / return, people and library cards, barcodes and settings, export / import,
|
||||||
@@ -285,3 +290,27 @@ points, quiz number and interest level fill in by themselves; check them and pre
|
|||||||
The book page's AR box has **Open beside this page** (a pop-up window next to the book) and **Show it here** (AR BookFind inside the
|
The book page's AR box has **Open beside this page** (a pop-up window next to the book) and **Show it here** (AR BookFind inside the
|
||||||
page; some browsers block its cookies inside another page, then use the pop-up). Either copies the title first so you can paste
|
page; some browsers block its cookies inside another page, then use the pop-up). Either copies the title first so you can paste
|
||||||
it into AR's search; AR's own page cannot be pre-filled from here.
|
it into AR's search; AR's own page cannot be pre-filled from here.
|
||||||
|
|
||||||
|
### Same ports on every install
|
||||||
|
|
||||||
|
New installs of this stack use the same three ports whether you install it from the kiosk add-on or from `ubuntu-post-install`:
|
||||||
|
**8098** is the one to use (the Room for Books home page, with Koha behind the menu), 8099 is Add books direct, and 8097 is the
|
||||||
|
public catalogue, which stays closed. If one is taken, the next free port is used and the installer prints it. Existing installs
|
||||||
|
keep the ports they already have.
|
||||||
|
|
||||||
|
## HTTPS with Caddy (and the live camera scanner)
|
||||||
|
|
||||||
|
Phones only allow a live camera on an HTTPS page. One command adds Caddy in front of the library:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# a real domain that points at this machine (ports 80 and 443 reachable): the certificate is automatic
|
||||||
|
sudo ./install.sh --domain library.example.com
|
||||||
|
|
||||||
|
# a name only your own network knows (library.home.lan): Caddy makes its own certificate authority
|
||||||
|
sudo ./install.sh --domain library.home.lan --lan-tls # then install caddy-root.crt on your phones
|
||||||
|
```
|
||||||
|
|
||||||
|
It writes `Caddyfile`, starts a `caddy` container (the compose `caddy` profile), sets `STAFF_URL=https://<name>` and closes the plain-HTTP
|
||||||
|
ports to the network (`BIND_ADDR=127.0.0.1`; add `--keep-direct` to leave them open). Then use `https://<name>/`: everything works
|
||||||
|
as before, and the **Live scan** buttons appear on Add books and Lend / return. Behind your own Caddy instead, point it at the library
|
||||||
|
port: `library.example.com { reverse_proxy <this machine>:8098 }` and set `STAFF_URL=https://library.example.com` in `.env`.
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ services:
|
|||||||
KOHA_ITEM_TYPES: ${ITEM_TYPES:-}
|
KOHA_ITEM_TYPES: ${ITEM_TYPES:-}
|
||||||
ports:
|
ports:
|
||||||
# The public catalogue is off by default (loopback only); set OPAC_BIND=0.0.0.0 in .env to expose it.
|
# The public catalogue is off by default (loopback only); set OPAC_BIND=0.0.0.0 in .env to expose it.
|
||||||
- "${OPAC_BIND:-127.0.0.1}:${OPAC_PORT:-8080}:80"
|
- "${OPAC_BIND:-127.0.0.1}:${OPAC_PORT:-8097}:80"
|
||||||
- "${BIND_ADDR:-0.0.0.0}:${STAFF_PORT:-8081}:8081"
|
- "${BIND_ADDR:-0.0.0.0}:${STAFF_PORT:-8098}:8081"
|
||||||
volumes:
|
volumes:
|
||||||
- koha-data:/var/lib/koha
|
- koha-data:/var/lib/koha
|
||||||
|
|
||||||
@@ -67,17 +67,36 @@ services:
|
|||||||
VIEWER_USER: ${VIEWER_USER:-guest}
|
VIEWER_USER: ${VIEWER_USER:-guest}
|
||||||
VIEWER_PASSWORD: ${VIEWER_PASSWORD:-}
|
VIEWER_PASSWORD: ${VIEWER_PASSWORD:-}
|
||||||
KOHA_LIBRARY: ${LIBRARY_CODE:-HOME}
|
KOHA_LIBRARY: ${LIBRARY_CODE:-HOME}
|
||||||
OPAC_PORT: ${OPAC_PORT:-8080}
|
OPAC_PORT: ${OPAC_PORT:-8097}
|
||||||
STAFF_PORT: ${STAFF_PORT:-8081}
|
STAFF_PORT: ${STAFF_PORT:-8098}
|
||||||
OPAC_URL: ${OPAC_URL:-}
|
OPAC_URL: ${OPAC_URL:-}
|
||||||
STAFF_URL: ${STAFF_URL:-}
|
STAFF_URL: ${STAFF_URL:-}
|
||||||
QUICKADD_SOURCES: ${QUICKADD_SOURCES:-z3950:lx2.loc.gov:210/LCDB,openlibrary}
|
QUICKADD_SOURCES: ${QUICKADD_SOURCES:-z3950:lx2.loc.gov:210/LCDB,openlibrary}
|
||||||
ports:
|
ports:
|
||||||
- "${BIND_ADDR:-0.0.0.0}:${QUICKADD_PORT:-8082}:8082"
|
- "${BIND_ADDR:-0.0.0.0}:${QUICKADD_PORT:-8099}:8082"
|
||||||
volumes:
|
volumes:
|
||||||
- quickadd-data:/data
|
- quickadd-data:/data
|
||||||
|
|
||||||
|
# HTTPS with an automatic certificate (and so the live camera on phones): only started when
|
||||||
|
# `./install.sh --domain <name>` has set COMPOSE_PROFILES=caddy in .env.
|
||||||
|
caddy:
|
||||||
|
profiles: ["caddy"]
|
||||||
|
image: caddy:2
|
||||||
|
container_name: library-caddy
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- koha
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- caddy-data:/data
|
||||||
|
- caddy-config:/config
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db-data:
|
db-data:
|
||||||
koha-data:
|
koha-data:
|
||||||
quickadd-data:
|
quickadd-data:
|
||||||
|
caddy-data:
|
||||||
|
caddy-config:
|
||||||
|
|||||||
+68
-6
@@ -5,13 +5,16 @@
|
|||||||
# sudo ./install.sh --status # show URLs and container health
|
# sudo ./install.sh --status # show URLs and container health
|
||||||
# sudo ./install.sh --update # rebuild with the latest Koha and restart
|
# sudo ./install.sh --update # rebuild with the latest Koha and restart
|
||||||
# sudo ./install.sh --stop | --start
|
# sudo ./install.sh --stop | --start
|
||||||
|
# sudo ./install.sh --domain library.example.com # HTTPS via Caddy (certificate is automatic)
|
||||||
|
# sudo ./install.sh --domain library.home.lan --lan-tls # HTTPS for a name only your network knows (own CA)
|
||||||
|
# add --keep-direct to also keep the plain-HTTP ports open on the network
|
||||||
#
|
#
|
||||||
# Optional environment variables:
|
# Optional environment variables:
|
||||||
# LIBRARY_NAME="Smith Family Library" name shown on the catalogue
|
# LIBRARY_NAME="Smith Family Library" name shown on the catalogue
|
||||||
# LIBRARY_CODE=HOME short code for the library (letters/digits)
|
# LIBRARY_CODE=HOME short code for the library (letters/digits)
|
||||||
# SHELVES="LR=Living room;BR=Bedroom" shelves/rooms to offer (first install only)
|
# SHELVES="LR=Living room;BR=Bedroom" shelves/rooms to offer (first install only)
|
||||||
# ITEM_TYPES="CD=Compact disc" extra item types (first install only)
|
# ITEM_TYPES="CD=Compact disc" extra item types (first install only)
|
||||||
# OPAC_PORT=8080 STAFF_PORT=8081 preferred host ports (next free is used)
|
# OPAC_PORT=8097 STAFF_PORT=8098 preferred host ports (next free is used)
|
||||||
# EXTRA_CA=/path/to/proxy-ca.crt trust an extra CA while building
|
# EXTRA_CA=/path/to/proxy-ca.crt trust an extra CA while building
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -79,6 +82,10 @@ show_status() {
|
|||||||
fi
|
fi
|
||||||
dc ps
|
dc ps
|
||||||
echo
|
echo
|
||||||
|
if [ -n "${DOMAIN:-}" ]; then
|
||||||
|
echo " Your library (HTTPS): https://$DOMAIN <- use this one: the live camera scanner needs it"
|
||||||
|
echo " (Koha's own screens: https://$DOMAIN/cgi-bin/koha/mainpage.pl, or the menu inside)"
|
||||||
|
fi
|
||||||
echo " Your library: http://$ip:$STAFF_PORT"
|
echo " Your library: http://$ip:$STAFF_PORT"
|
||||||
echo " sign in here: find a book, add books (scan an ISBN or use the phone camera), lending, labels, people"
|
echo " sign in here: find a book, add books (scan an ISBN or use the phone camera), lending, labels, people"
|
||||||
echo " Admin login: $ADMIN_USER / $ADMIN_PASSWORD (also stored in $HERE/.env)"
|
echo " Admin login: $ADMIN_USER / $ADMIN_PASSWORD (also stored in $HERE/.env)"
|
||||||
@@ -90,6 +97,52 @@ show_status() {
|
|||||||
echo
|
echo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- HTTPS (Caddy) options --------------------------------------------------------
|
||||||
|
DOMAIN_ARG="" LAN_TLS=0 KEEP_DIRECT=0 ACTION=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--domain) DOMAIN_ARG="${2:-}"; [ -n "$DOMAIN_ARG" ] || { c_err "--domain needs a name"; exit 1; }; shift 2 ;;
|
||||||
|
--lan-tls) LAN_TLS=1; shift ;;
|
||||||
|
--keep-direct) KEEP_DIRECT=1; shift ;;
|
||||||
|
*) ACTION="${ACTION:+$ACTION }$1"; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
set -- $ACTION
|
||||||
|
if [ -n "$DOMAIN_ARG" ] && ! printf '%s' "$DOMAIN_ARG" | grep -Eq '^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$'; then
|
||||||
|
c_err "That does not look like a host name: $DOMAIN_ARG"; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Write (or replace) KEY=VALUE in .env
|
||||||
|
env_set() { if grep -q "^$1=" .env; then sed -i "s|^$1=.*|$1=$2|" .env; else echo "$1=$2" >> .env; fi; }
|
||||||
|
|
||||||
|
configure_caddy() {
|
||||||
|
local name="$DOMAIN_ARG"
|
||||||
|
# Caddy needs 80 and 443 (it is not moved to other ports: browsers expect these).
|
||||||
|
if ! dc ps --status running --services 2>/dev/null | grep -qx caddy; then
|
||||||
|
for p in 80 443; do
|
||||||
|
port_in_use "$p" && { c_err "Port $p is already in use on this machine - Caddy needs it. Stop what is using it, or use your own reverse proxy (see README)."; exit 1; }
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
{
|
||||||
|
echo "# Generated by install.sh --domain - Caddy fetches and renews the certificate by itself."
|
||||||
|
echo "$name {"
|
||||||
|
[ "$LAN_TLS" = 1 ] && echo " tls internal"
|
||||||
|
echo " encode zstd gzip"
|
||||||
|
echo " reverse_proxy koha:8081"
|
||||||
|
echo "}"
|
||||||
|
} > Caddyfile
|
||||||
|
env_set DOMAIN "$name"
|
||||||
|
env_set COMPOSE_PROFILES caddy
|
||||||
|
env_set STAFF_URL "https://$name"
|
||||||
|
[ "$KEEP_DIRECT" = 1 ] || env_set BIND_ADDR 127.0.0.1
|
||||||
|
c_ok "Caddy will serve https://$name"
|
||||||
|
if [ "$LAN_TLS" = 1 ]; then
|
||||||
|
c_info "Own-CA certificate: phones must trust Caddy's root, which is copied to $HERE/caddy-root.crt after start."
|
||||||
|
else
|
||||||
|
c_info "The name must point at this machine (DNS) and ports 80/443 must reach it, or the certificate cannot be issued."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
--status) show_status; exit 0 ;;
|
--status) show_status; exit 0 ;;
|
||||||
--stop) dc stop; exit 0 ;;
|
--stop) dc stop; exit 0 ;;
|
||||||
@@ -117,9 +170,9 @@ LIBRARY_CODE=${LIBRARY_CODE:-HOME}
|
|||||||
SHELVES="${SHELVES:-}"
|
SHELVES="${SHELVES:-}"
|
||||||
ITEM_TYPES="${ITEM_TYPES:-}"
|
ITEM_TYPES="${ITEM_TYPES:-}"
|
||||||
TZ=${TZ_NOW}
|
TZ=${TZ_NOW}
|
||||||
OPAC_PORT=$(free_port "${OPAC_PORT:-8080}")
|
OPAC_PORT=$(free_port "${OPAC_PORT:-8097}")
|
||||||
STAFF_PORT=$(free_port "${STAFF_PORT:-8081}")
|
STAFF_PORT=$(free_port "${STAFF_PORT:-8098}")
|
||||||
QUICKADD_PORT=$(free_port "${QUICKADD_PORT:-8082}")
|
QUICKADD_PORT=$(free_port "${QUICKADD_PORT:-8099}")
|
||||||
BIND_ADDR=${BIND_ADDR:-0.0.0.0}
|
BIND_ADDR=${BIND_ADDR:-0.0.0.0}
|
||||||
# The public catalogue is off by default: 127.0.0.1 = not reachable from the network.
|
# The public catalogue is off by default: 127.0.0.1 = not reachable from the network.
|
||||||
OPAC_BIND=${OPAC_BIND:-127.0.0.1}
|
OPAC_BIND=${OPAC_BIND:-127.0.0.1}
|
||||||
@@ -139,7 +192,7 @@ else
|
|||||||
c_info "Re-using existing .env"
|
c_info "Re-using existing .env"
|
||||||
# Settings added in later versions: append them to an older .env.
|
# Settings added in later versions: append them to an older .env.
|
||||||
if ! grep -q '^QUICKADD_PORT=' .env; then
|
if ! grep -q '^QUICKADD_PORT=' .env; then
|
||||||
echo "QUICKADD_PORT=$(free_port 8082)" >> .env
|
echo "QUICKADD_PORT=$(free_port 8099)" >> .env
|
||||||
fi
|
fi
|
||||||
if ! grep -q '^VIEWER_PASSWORD=' .env; then
|
if ! grep -q '^VIEWER_PASSWORD=' .env; then
|
||||||
{ echo "VIEWER_USER=guest"; echo "VIEWER_PASSWORD=$(rand 14)"; } >> .env
|
{ echo "VIEWER_USER=guest"; echo "VIEWER_PASSWORD=$(rand 14)"; } >> .env
|
||||||
@@ -152,6 +205,8 @@ else
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
[ -n "$DOMAIN_ARG" ] && configure_caddy
|
||||||
|
|
||||||
if [ -n "${EXTRA_CA:-}" ]; then
|
if [ -n "${EXTRA_CA:-}" ]; then
|
||||||
cp "$EXTRA_CA" koha/certs/extra-ca.crt
|
cp "$EXTRA_CA" koha/certs/extra-ca.crt
|
||||||
cp "$EXTRA_CA" quickadd/certs/extra-ca.crt
|
cp "$EXTRA_CA" quickadd/certs/extra-ca.crt
|
||||||
@@ -170,7 +225,7 @@ with_retry dc up -d
|
|||||||
c_info "First start installs the database - waiting until Koha reports healthy..."
|
c_info "First start installs the database - waiting until Koha reports healthy..."
|
||||||
for i in $(seq 1 180); do
|
for i in $(seq 1 180); do
|
||||||
health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' library-koha 2>/dev/null || echo missing)"
|
health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' library-koha 2>/dev/null || echo missing)"
|
||||||
if [ "$health" = healthy ] && curl -fsS -o /dev/null -m 5 "http://127.0.0.1:${QUICKADD_PORT:-8082}/healthz" 2>/dev/null; then READY=1; break; fi
|
if [ "$health" = healthy ] && curl -fsS -o /dev/null -m 5 "http://127.0.0.1:${QUICKADD_PORT:-8099}/healthz" 2>/dev/null; then READY=1; break; fi
|
||||||
# One restart is tolerated (Docker's restart policy); a crash loop is not.
|
# One restart is tolerated (Docker's restart policy); a crash loop is not.
|
||||||
if [ "$(docker inspect -f '{{.RestartCount}}' library-koha 2>/dev/null || echo 99)" -ge 3 ]; then
|
if [ "$(docker inspect -f '{{.RestartCount}}' library-koha 2>/dev/null || echo 99)" -ge 3 ]; then
|
||||||
c_err "library-koha keeps crashing - see: docker logs library-koha"; exit 1
|
c_err "library-koha keeps crashing - see: docker logs library-koha"; exit 1
|
||||||
@@ -180,5 +235,12 @@ done
|
|||||||
[ "${READY:-0}" = 1 ] || { c_err "Timed out - see: docker logs -f library-koha"; exit 1; }
|
[ "${READY:-0}" = 1 ] || { c_err "Timed out - see: docker logs -f library-koha"; exit 1; }
|
||||||
|
|
||||||
c_ok "Your library is running."
|
c_ok "Your library is running."
|
||||||
|
if [ -n "${DOMAIN:-}" ] && grep -q '^ tls internal' Caddyfile 2>/dev/null; then
|
||||||
|
for i in $(seq 1 20); do
|
||||||
|
docker cp library-caddy:/data/caddy/pki/authorities/local/root.crt "$HERE/caddy-root.crt" >/dev/null 2>&1 && break
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
[ -f "$HERE/caddy-root.crt" ] && c_info "Install this certificate on phones/computers that should trust https://$DOMAIN: $HERE/caddy-root.crt"
|
||||||
|
fi
|
||||||
echo
|
echo
|
||||||
show_status
|
show_status
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ if [ ! -d "/etc/koha/sites/$INST" ]; then
|
|||||||
cat > /etc/koha/apache-quickadd.conf << APACHE
|
cat > /etc/koha/apache-quickadd.conf << APACHE
|
||||||
<IfModule mod_proxy_http.c>
|
<IfModule mod_proxy_http.c>
|
||||||
RedirectMatch ^/add\$ /add/
|
RedirectMatch ^/add\$ /add/
|
||||||
|
# The bare address (http://ip:port/ or https://your-domain/) opens the Room for Books home page;
|
||||||
|
# Koha's own staff screens stay at /cgi-bin/koha/mainpage.pl.
|
||||||
|
RedirectMatch ^/\$ /add/
|
||||||
ProxyPass /add/ ${KOHA_QUICKADD_URL%/}/
|
ProxyPass /add/ ${KOHA_QUICKADD_URL%/}/
|
||||||
ProxyPassReverse /add/ ${KOHA_QUICKADD_URL%/}/
|
ProxyPassReverse /add/ ${KOHA_QUICKADD_URL%/}/
|
||||||
${SSO_BLOCK}
|
${SSO_BLOCK}
|
||||||
|
|||||||
Binary file not shown.
@@ -985,7 +985,8 @@ def _loan_rows(cos):
|
|||||||
due = (c.get("due_date") or "")[:10]
|
due = (c.get("due_date") or "")[:10]
|
||||||
rows.append({"checkout_id": c["checkout_id"], "patron_id": c["patron_id"], "barcode": it.get("external_id") or "",
|
rows.append({"checkout_id": c["checkout_id"], "patron_id": c["patron_id"], "barcode": it.get("external_id") or "",
|
||||||
"biblio_id": it.get("biblio_id"), "title": t[0], "author": t[1], "isbn": t[2], "due": due,
|
"biblio_id": it.get("biblio_id"), "title": t[0], "author": t[1], "isbn": t[2], "due": due,
|
||||||
"overdue": bool(due) and due < today, "renewals": c.get("renewals_count") or 0})
|
"overdue": bool(due) and due < today, "renewals": c.get("renewals_count") or 0,
|
||||||
|
"checkout_date": (c.get("checkout_date") or "")[:10]})
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
@@ -1719,6 +1720,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_header("Content-Length", str(len(body)))
|
self.send_header("Content-Length", str(len(body)))
|
||||||
self.send_header("Cache-Control", "no-store")
|
self.send_header("Cache-Control", "no-store")
|
||||||
self.send_header("X-Content-Type-Options", "nosniff")
|
self.send_header("X-Content-Type-Options", "nosniff")
|
||||||
|
self.send_header("Permissions-Policy", "camera=(self), microphone=()")
|
||||||
self.send_header("Referrer-Policy", "no-referrer")
|
self.send_header("Referrer-Policy", "no-referrer")
|
||||||
self.send_header("Content-Security-Policy",
|
self.send_header("Content-Security-Policy",
|
||||||
"default-src 'self'; img-src 'self' https: data:; "
|
"default-src 'self'; img-src 'self' https: data:; "
|
||||||
@@ -1753,7 +1755,11 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
# Otherwise: a patron signing in with their card number (or userid) and password.
|
# Otherwise: a patron signing in with their card number (or userid) and password.
|
||||||
addr = self.client_address[0]
|
addr = self.client_address[0]
|
||||||
if SSO_SECRET and hmac.compare_digest(self.headers.get("X-Quickadd-Proxy", "").encode(), SSO_SECRET.encode()):
|
if SSO_SECRET and hmac.compare_digest(self.headers.get("X-Quickadd-Proxy", "").encode(), SSO_SECRET.encode()):
|
||||||
addr = self.headers.get("X-Forwarded-For", "").split(",")[-1].strip() or addr
|
# Behind Caddy the list reads "<visitor>, <Caddy>" (Caddy adds the visitor, the staff site's web server
|
||||||
|
# adds Caddy), so the visitor is the one before the last. Used only to count failed sign-ins per
|
||||||
|
# visitor, never to vouch for anyone.
|
||||||
|
hops = [h.strip() for h in self.headers.get("X-Forwarded-For", "").split(",") if h.strip()]
|
||||||
|
addr = (hops[-2] if len(hops) >= 2 else hops[-1]) if hops else addr
|
||||||
self.addr = addr
|
self.addr = addr
|
||||||
pid = patron_login(u, p, addr)
|
pid = patron_login(u, p, addr)
|
||||||
if pid:
|
if pid:
|
||||||
|
|||||||
+28
-11
@@ -201,6 +201,7 @@
|
|||||||
<button class="primary" type="submit">Look up</button>
|
<button class="primary" type="submit">Look up</button>
|
||||||
</form>
|
</form>
|
||||||
<div id="homeresults" aria-live="polite"></div>
|
<div id="homeresults" aria-live="polite"></div>
|
||||||
|
<div id="homemine" hidden></div>
|
||||||
<div class="bigacts" id="homeacts">
|
<div class="bigacts" id="homeacts">
|
||||||
<a class="bigact" href="#lend"><strong>Check out</strong><span>Scan a card, then the books</span></a>
|
<a class="bigact" href="#lend"><strong>Check out</strong><span>Scan a card, then the books</span></a>
|
||||||
<a class="bigact" href="#return"><strong>Return</strong><span>Scan the books coming back</span></a>
|
<a class="bigact" href="#return"><strong>Return</strong><span>Scan the books coming back</span></a>
|
||||||
@@ -1243,21 +1244,37 @@
|
|||||||
};
|
};
|
||||||
$('printcard').onclick = () => window.print();
|
$('printcard').onclick = () => window.print();
|
||||||
|
|
||||||
async function loadMine() {
|
// A borrower's loans: title, who wrote it, when it was borrowed, when it is due (days left / overdue), renewals, Renew.
|
||||||
let r; try { r = await api('/api/me'); } catch (e) { r = {}; }
|
function renderLoans(target, r, reload) {
|
||||||
if (!r.loans) { $('meloans').textContent = r.message || 'Could not load your books.'; return; }
|
|
||||||
$('mename').textContent = r.name; $('mecard').textContent = r.card;
|
|
||||||
const today = new Date(); today.setHours(12, 0, 0, 0);
|
const today = new Date(); today.setHours(12, 0, 0, 0);
|
||||||
const rows = r.loans.map(l => {
|
const rows = r.loans.map(l => {
|
||||||
const days = Math.round((new Date(l.due + 'T12:00:00') - today) / 86400000);
|
const days = Math.round((new Date(l.due + 'T12:00:00') - today) / 86400000);
|
||||||
const when = l.overdue ? 'Overdue by ' + (-days) + (days === -1 ? ' day' : ' days') : days === 0 ? 'Due today' : 'Due ' + fmtDue(l.due) + ' (in ' + days + (days === 1 ? ' day)' : ' days)');
|
const when = l.overdue ? 'Overdue by ' + (-days) + (days === -1 ? ' day' : ' days') : days === 0 ? 'Due today' : 'Due ' + fmtDue(l.due) + ' (in ' + days + (days === 1 ? ' day)' : ' days)');
|
||||||
const ren = el('button', { type: 'button' }, 'Renew');
|
const ren = el('button', { type: 'button' }, 'Renew');
|
||||||
ren.onclick = async () => { ren.disabled = true; const x = await api('/api/me/renew', { checkout_id: l.checkout_id });
|
ren.onclick = async () => { ren.disabled = true; const x = await api('/api/me/renew', { checkout_id: l.checkout_id });
|
||||||
if (x.status === 'renewed') loadMine(); else { ren.disabled = false; alert(x.message || 'That one cannot be renewed.'); } };
|
if (x.status === 'renewed') reload(); else { ren.disabled = false; alert(x.message || 'That one cannot be renewed (the limit is reached, or someone is waiting for it).'); } };
|
||||||
return el('div', { className: 'loan' }, el('span', { className: 't' }, el('a', { href: '#book/' + l.biblio_id }, l.title), el('div', { className: 'small' }, l.author)),
|
const facts = ['Borrowed ' + fmtDue(l.checkout_date), l.renewals ? 'renewed ' + (l.renewals === 1 ? 'once' : l.renewals + ' times') : ''].filter(Boolean).join(' · ');
|
||||||
el('span', { className: 'chip ' + (l.overdue ? 'late' : 'in') }, when), ren);
|
return el('div', { className: 'loan' }, el('span', { className: 't' }, el('a', { href: '#book/' + l.biblio_id }, l.title),
|
||||||
|
el('div', { className: 'small' }, [l.author, facts].filter(Boolean).join(' · '))),
|
||||||
|
el('span', { className: 'chip ' + (l.overdue ? 'late' : days <= 3 ? 'out' : 'in') }, when), ren);
|
||||||
});
|
});
|
||||||
$('meloans').replaceChildren(...(rows.length ? rows : [el('div', { className: 'card ok' }, el('div', { className: 'body' }, 'You have nothing checked out right now.'))]));
|
const late = r.loans.filter(l => l.overdue).length;
|
||||||
|
const head = r.loans.length ? 'You have ' + r.loans.length + (r.loans.length === 1 ? ' book' : ' books') + ' out' + (late ? ', ' + late + ' overdue' : '') + '.' : 'You have nothing checked out right now.';
|
||||||
|
target.replaceChildren(el('div', { className: 'card ' + (late ? 'warn' : 'ok') }, el('div', { className: 'body' }, el('strong', {}, head))), ...rows);
|
||||||
|
}
|
||||||
|
async function loadMine() {
|
||||||
|
let r; try { r = await api('/api/me'); } catch (e) { r = {}; }
|
||||||
|
if (!r.loans) { $('meloans').textContent = r.message || 'Could not load your books.'; return; }
|
||||||
|
$('mename').textContent = r.name; $('mecard').textContent = r.card;
|
||||||
|
renderLoans($('meloans'), r, loadMine);
|
||||||
|
}
|
||||||
|
async function loadHomeMine() {
|
||||||
|
let r; try { r = await api('/api/me'); } catch (e) { return; }
|
||||||
|
if (!r.loans) return;
|
||||||
|
const box = $('homemine'); box.hidden = false;
|
||||||
|
const inner = el('div', {});
|
||||||
|
box.replaceChildren(el('h2', {}, 'Hello, ' + r.name), inner);
|
||||||
|
renderLoans(inner, r, loadHomeMine);
|
||||||
}
|
}
|
||||||
$('mepwform').onsubmit = async ev => {
|
$('mepwform').onsubmit = async ev => {
|
||||||
ev.preventDefault(); const f = ev.target; const r = await api('/api/me/password', Object.fromEntries(new FormData(f)));
|
ev.preventDefault(); const f = ev.target; const r = await api('/api/me/password', Object.fromEntries(new FormData(f)));
|
||||||
@@ -1324,7 +1341,7 @@
|
|||||||
const names = { home: 'Home', library: 'Browse', book: 'Book', add: 'Add books', lend: 'Lend / return', people: 'People', card: 'Library card', cards: 'Library cards', mine: 'My books' };
|
const names = { home: 'Home', library: 'Browse', book: 'Book', add: 'Add books', lend: 'Lend / return', people: 'People', card: 'Library card', cards: 'Library cards', mine: 'My books' };
|
||||||
document.title = (view === 'home' ? '' : names[view] + ' - ') + 'Room for Books';
|
document.title = (view === 'home' ? '' : names[view] + ' - ') + 'Room for Books';
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
if (view === 'home') { $('homeacts').hidden = viewer; $('patronacts').hidden = !isPatron; loadRecent(); if (h === 'find') $('hq').focus(); }
|
if (view === 'home') { $('homeacts').hidden = viewer; $('patronacts').hidden = !isPatron; $('homemine').hidden = !isPatron; if (isPatron) loadHomeMine(); loadRecent(); if (h === 'find') $('hq').focus(); }
|
||||||
else if (view === 'book') loadBook(m[1]);
|
else if (view === 'book') loadBook(m[1]);
|
||||||
else if (view === 'mine') loadMine();
|
else if (view === 'mine') loadMine();
|
||||||
else if (view === 'add') { if (h === 'settings') { $('settings').open = true; $('settings').scrollIntoView(); } else $('code').focus(); }
|
else if (view === 'add') { if (h === 'settings') { $('settings').open = true; $('settings').scrollIntoView(); } else $('code').focus(); }
|
||||||
@@ -1347,8 +1364,8 @@
|
|||||||
// The public catalogue is off by default: only link to it when a public address was configured.
|
// The public catalogue is off by default: only link to it when a public address was configured.
|
||||||
$('lnk-opac').hidden = !s.opac_url;
|
$('lnk-opac').hidden = !s.opac_url;
|
||||||
if (s.opac_url) $('lnk-opac').href = s.opac_url + '/';
|
if (s.opac_url) $('lnk-opac').href = s.opac_url + '/';
|
||||||
$('lnk-staff').href = staffRoot() + '/';
|
$('lnk-staff').href = staffRoot() + '/cgi-bin/koha/mainpage.pl';
|
||||||
$('lnk-staff').hidden = viewer; $('footkoha').href = staffRoot() + '/';
|
$('lnk-staff').hidden = viewer; $('footkoha').href = staffRoot() + '/cgi-bin/koha/mainpage.pl';
|
||||||
$('footname').textContent = 'Room for Books' + (s.library_name ? ' · ' + s.library_name : '');
|
$('footname').textContent = 'Room for Books' + (s.library_name ? ' · ' + s.library_name : '');
|
||||||
$('lnk-labels').href = staffRoot() + '/cgi-bin/koha/labels/label-edit-range.pl';
|
$('lnk-labels').href = staffRoot() + '/cgi-bin/koha/labels/label-edit-range.pl';
|
||||||
if (s.locations && s.locations.length) {
|
if (s.locations && s.locations.length) {
|
||||||
|
|||||||
Reference in New Issue
Block a user