diff --git a/library/docker-compose.yml b/library/docker-compose.yml index 40dd3ed..77009fa 100644 --- a/library/docker-compose.yml +++ b/library/docker-compose.yml @@ -44,7 +44,8 @@ services: KOHA_SHELVES: ${SHELVES:-} KOHA_ITEM_TYPES: ${ITEM_TYPES:-} ports: - - "${BIND_ADDR:-0.0.0.0}:${OPAC_PORT:-8080}:80" + # 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" - "${BIND_ADDR:-0.0.0.0}:${STAFF_PORT:-8081}:8081" volumes: - koha-data:/var/lib/koha @@ -61,6 +62,8 @@ services: KOHA_API: http://koha:8081/api/v1 ADMIN_USER: ${ADMIN_USER:-admin} ADMIN_PASSWORD: ${ADMIN_PASSWORD} + VIEWER_USER: ${VIEWER_USER:-guest} + VIEWER_PASSWORD: ${VIEWER_PASSWORD:-} KOHA_LIBRARY: ${LIBRARY_CODE:-HOME} OPAC_PORT: ${OPAC_PORT:-8080} STAFF_PORT: ${STAFF_PORT:-8081} diff --git a/library/install.sh b/library/install.sh index ebedaa2..da63d67 100755 --- a/library/install.sh +++ b/library/install.sh @@ -80,9 +80,13 @@ show_status() { dc ps echo echo " Your library: http://$ip:$STAFF_PORT" - echo " sign in here: add books at /add/ (or the 'Add books' button), find a book, lending, labels, people" - echo " Public catalogue: http://$ip:$OPAC_PORT (search only - for a kiosk or visitors)" - echo " Login: $ADMIN_USER / $ADMIN_PASSWORD (also stored in $HERE/.env; Add books asks once more with the same login)" + 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 " Read-only login: ${VIEWER_USER:-guest} / ${VIEWER_PASSWORD:-?} (to hand out, with this address: http://$ip:$STAFF_PORT/add/)" + echo " It opens the Find a book page: search only, it cannot add or change anything." + if [ "${OPAC_BIND:-127.0.0.1}" != "127.0.0.1" ]; then + echo " Public catalogue: http://$ip:$OPAC_PORT (search only, asks for a library card login)" + fi echo } @@ -117,8 +121,12 @@ OPAC_PORT=$(free_port "${OPAC_PORT:-8080}") STAFF_PORT=$(free_port "${STAFF_PORT:-8081}") QUICKADD_PORT=$(free_port "${QUICKADD_PORT:-8082}") BIND_ADDR=${BIND_ADDR:-0.0.0.0} +# The public catalogue is off by default: 127.0.0.1 = not reachable from the network. +OPAC_BIND=${OPAC_BIND:-127.0.0.1} ADMIN_USER=admin ADMIN_PASSWORD=$(rand 16) +VIEWER_USER=guest +VIEWER_PASSWORD=$(rand 14) DB_ROOT_PASSWORD=$(rand 32) DB_PASSWORD=$(rand 32) KOHA_SUITE=stable @@ -131,6 +139,11 @@ else if ! grep -q '^QUICKADD_PORT=' .env; then echo "QUICKADD_PORT=$(free_port 8082)" >> .env fi + if ! grep -q '^VIEWER_PASSWORD=' .env; then + { echo "VIEWER_USER=guest"; echo "VIEWER_PASSWORD=$(rand 14)"; } >> .env + fi + # Existing installs keep their current public-catalogue behaviour. + grep -q '^OPAC_BIND=' .env || echo "OPAC_BIND=0.0.0.0" >> .env if ! grep -q '^BIND_ADDR=' .env; then echo "BIND_ADDR=${BIND_ADDR:-0.0.0.0}" >> .env fi diff --git a/library/koha/Dockerfile b/library/koha/Dockerfile index eb69ff8..5dda088 100644 --- a/library/koha/Dockerfile +++ b/library/koha/Dockerfile @@ -39,7 +39,7 @@ RUN for c in koha-create koha-plack koha-zebra koha-rebuild-zebra koha-upgrade-s command -v "$c" >/dev/null || { echo "BUILD ERROR: $c is missing from the image" >&2; exit 1; }; \ done -COPY bootstrap.pl entrypoint.sh /opt/koha-init/ +COPY bootstrap.pl ensure.pl entrypoint.sh /opt/koha-init/ COPY seed/ /opt/koha-init/seed/ RUN chmod 0755 /opt/koha-init/entrypoint.sh && chmod -R a+rX /opt/koha-init diff --git a/library/koha/bootstrap.pl b/library/koha/bootstrap.pl index 74f00b5..deebb7b 100755 --- a/library/koha/bootstrap.pl +++ b/library/koha/bootstrap.pl @@ -153,19 +153,6 @@ Koha::CirculationRules->set_rules({ }, }); -# ---- Staff home page: a "Library tools" block with a link to the Add books page ---- -# (Koha 26.05 stores these in the HTML customizations tool, not in a preference.) -{ - my $html = '

Library tools

'; - $dbh->do(q{INSERT INTO additional_contents (category, code, location, branchcode, published_on, number) - VALUES ('html_customizations', 'library_tools', 'IntranetmainUserblock', NULL, CURDATE(), 1)}); - my $id = $dbh->last_insert_id( undef, undef, 'additional_contents', 'id' ); - $dbh->do( q{INSERT INTO additional_contents_localizations (additional_content_id, title, content, lang) - VALUES (?, 'Library tools', ?, 'default')}, undef, $id, $html ); -} - # ---- Preferences ---------------------------------------------------------- my %prefs = ( autoBarcode => 'OFF', # barcodes are assigned by Quick Add (zero-padded, or none/scanned on request) @@ -173,11 +160,11 @@ my %prefs = ( finesMode => 'off', # a home library does not charge overdue fines itemcallnumber => '082ab', # Dewey call number copied from the MARC record RESTBasicAuth => 1, # lets the quick-add page use the REST API + OpacPublic => 0, # the public catalogue, if ever exposed, asks for a login OpenLibraryCovers => 1, # cover images in the catalogue OPACSearchForTitleIn => '', BiblioDefaultView => 'normal', - OpacPublic => 1, - opacuserlogin => 0, # catalogue is browse-only; no patron logins needed + opacuserlogin => 1, OpacMaintenance => 0, ExpireReservesMaxPickUpDelay => 0, AllowOfflineCirculation => 0, diff --git a/library/koha/ensure.pl b/library/koha/ensure.pl new file mode 100644 index 0000000..5ef74a6 --- /dev/null +++ b/library/koha/ensure.pl @@ -0,0 +1,35 @@ +#!/usr/bin/perl +# Idempotent housekeeping, run on EVERY container start (after the first-time install or +# the schema upgrade), so existing installs pick these up too: +# * removal of the Koha guest account an earlier version created (the guest login is Quick Add only) +# * the "Library tools" block on the staff home page +use Modern::Perl; +use C4::Context; +use Koha::Patrons; +use Koha::Patron::Categories; + +my $dbh = C4::Context->dbh; +# The read-only "guest" login lives in Quick Add only (the Find a book page), NOT in Koha: +# Koha's own "catalogue" permission also allows editing label layouts and templates, so a +# Koha account cannot be made truly read-only. Remove any guest account an earlier version created. +for my $p ( Koha::Patrons->search( { categorycode => 'VIEWER' } )->as_list ) { + say "ensure: removing the old Koha guest account '" . $p->userid . "'"; + $p->delete; +} +if ( my $cat = Koha::Patron::Categories->find('VIEWER') ) { + $cat->delete unless Koha::Patrons->search( { categorycode => 'VIEWER' } )->count; +} + +# Staff home page block (Koha 26.05 keeps these in the HTML customizations tool). +my ($have) = $dbh->selectrow_array(q{SELECT COUNT(*) FROM additional_contents WHERE code = 'library_tools'}); +unless ($have) { + my $html = '

Library tools

'; + $dbh->do(q{INSERT INTO additional_contents (category, code, location, branchcode, published_on, number) + VALUES ('html_customizations', 'library_tools', 'IntranetmainUserblock', NULL, CURDATE(), 1)}); + my $id = $dbh->last_insert_id( undef, undef, 'additional_contents', 'id' ); + $dbh->do( q{INSERT INTO additional_contents_localizations (additional_content_id, title, content, lang) + VALUES (?, 'Library tools', ?, 'default')}, undef, $id, $html ); + say "ensure: added the staff home 'Library tools' block"; +} diff --git a/library/koha/entrypoint.sh b/library/koha/entrypoint.sh index 34d5bf0..c1532c1 100755 --- a/library/koha/entrypoint.sh +++ b/library/koha/entrypoint.sh @@ -108,6 +108,11 @@ else FIRST_RUN=0 fi +# --- 4b. housekeeping that must hold on every start (staff home block, old guest account cleanup) +runuser -u "$INST-koha" -- env KOHA_CONF="/etc/koha/sites/$INST/koha-conf.xml" PERL5LIB=/usr/share/koha/lib \ + perl /opt/koha-init/ensure.pl \ + || log "WARNING: ensure.pl reported a problem (see above)" + # --- 5. background services (their `daemon` processes die with the container, # so they are started explicitly on every start) ------------------------ if [ "$FIRST_RUN" = 1 ] || [ ! -e "/var/lib/koha/$INST/biblios/register" ]; then diff --git a/library/quickadd/app.py b/library/quickadd/app.py index ec2d1d7..f82a644 100644 --- a/library/quickadd/app.py +++ b/library/quickadd/app.py @@ -27,6 +27,9 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer KOHA_API = os.environ.get("KOHA_API", "http://koha:8081/api/v1").rstrip("/") USER = os.environ.get("ADMIN_USER", "admin") PASSWORD = os.environ.get("ADMIN_PASSWORD", "") +# Optional read-only login to hand out: may open the page and search, nothing else. +VIEWER_USER = os.environ.get("VIEWER_USER", "") +VIEWER_PASSWORD = os.environ.get("VIEWER_PASSWORD", "") LIBRARY = os.environ.get("KOHA_LIBRARY", "HOME") STAFF_PORT = os.environ.get("STAFF_PORT", "8081") OPAC_PORT = os.environ.get("OPAC_PORT", "8080") @@ -802,37 +805,50 @@ class Handler(BaseHTTPRequestHandler): self.end_headers() self.wfile.write(body) - def _authed(self): + def _role(self): + """'admin', 'viewer' (read-only guest) or None.""" if not PASSWORD: - return False + return None header = self.headers.get("Authorization", "") - if header.startswith("Basic "): - try: - u, _, p = base64.b64decode(header[6:]).decode().partition(":") - ok_u = hmac.compare_digest(u.encode(), USER.encode()) - ok_p = hmac.compare_digest(p.encode(), PASSWORD.encode()) - return ok_u and ok_p - except (ValueError, UnicodeDecodeError): - return False - return False + if not header.startswith("Basic "): + return None + try: + u, _, p = base64.b64decode(header[6:]).decode().partition(":") + except (ValueError, UnicodeDecodeError): + return None + ub, pb = u.encode(), p.encode() + # Both comparisons always run, so timing does not reveal which account exists. + is_admin = hmac.compare_digest(ub, USER.encode()) & hmac.compare_digest(pb, PASSWORD.encode()) + is_viewer = bool(VIEWER_USER and VIEWER_PASSWORD) & hmac.compare_digest(ub, VIEWER_USER.encode()) \ + & hmac.compare_digest(pb, VIEWER_PASSWORD.encode()) + return "admin" if is_admin else "viewer" if is_viewer else None - def _guard(self): - if not self._authed(): + def _guard(self, need_admin=True): + role = self._role() + if role is None: self._send(401, b"Login required", "text/plain", - {"WWW-Authenticate": 'Basic realm="Library quick add"'}) + {"WWW-Authenticate": 'Basic realm="Library"'}) return False + if need_admin and role != "admin": + self._send(403, {"status": "error", "message": "This login can search but not change anything."}) + return False + self.role = role return True def do_GET(self): if self.path == "/healthz": return self._send(200, b"ok", "text/plain") - if not self._guard(): - return path = self.path.split("?")[0] + if not self._guard(need_admin=path not in ("/", "/api/state", "/api/search")): + return if path == "/": return self._send(200, INDEX, "text/html; charset=utf-8") + if path == "/api/state" and self.role == "viewer": + return self._send(200, {"role": "viewer", "library": LIBRARY, "recent": [], "types": {}, "locations": [], + "settings": {}, "staff_port": STAFF_PORT, "staff_url": STAFF_URL, + "opac_url": OPAC_URL, "opac_port": OPAC_PORT, "sources": []}) if path == "/api/state": - return self._send(200, {"recent": load_recent()[:100], "types": get_item_types(), + return self._send(200, {"role": "admin", "recent": load_recent()[:100], "types": get_item_types(), "library": LIBRARY, "locations": get_locations(), "settings": load_settings(), "staff_port": STAFF_PORT, "staff_url": STAFF_URL, "opac_url": OPAC_URL, "opac_port": OPAC_PORT, "sources": [s.split(":", 1)[0] if s.startswith("z3950") else s for s in SOURCES]}) if path == "/api/settings": diff --git a/library/quickadd/index.html b/library/quickadd/index.html index db94b4f..5d9306c 100644 --- a/library/quickadd/index.html +++ b/library/quickadd/index.html @@ -455,10 +455,12 @@ // ---- under the staff site (/add/) everything is same-origin const embedded = location.pathname.startsWith('/add/'); + let viewer = false; // the read-only guest login: search only // ---- views: Add books / Find a book function showView() { - const find = location.hash === '#find'; + const find = viewer || location.hash === '#find'; + $('tab-add').hidden = viewer; $('view-add').hidden = find; $('view-find').hidden = !find; $('tab-add').classList.toggle('on', !find); $('tab-find').classList.toggle('on', find); $('title').textContent = find ? 'Find a book' : 'Add to library'; @@ -482,18 +484,22 @@ el('span', {}, c.location || 'no shelf set'), c.callnumber ? el('span', { className: 'meta' }, c.callnumber) : '', el('span', { className: 'chip ' + (c.status === 'On shelf' ? 'in' : 'out') }, c.status))) : [el('div', { className: 'small' }, 'No copies recorded.')]), - el('div', { className: 'small', style: 'margin-top:6px' }, + viewer ? '' : el('div', { className: 'small', style: 'margin-top:6px' }, el('a', { href: staffRoot() + '/cgi-bin/koha/catalogue/detail.pl?biblionumber=' + b.biblio_id, target: '_blank', rel: 'noopener' }, 'Open full record'))))); } $('findform').onsubmit = ev => { ev.preventDefault(); find($('q').value); }; (async () => { const s = await api('/api/state'); - types = s.types; if (!types[type]) type = 'BOOK'; typeButtons(); + viewer = s.role === 'viewer'; + types = s.types || {}; if (!types[type]) type = 'BOOK'; if (!viewer) typeButtons(); staffPort = s.staff_port; settings = s.settings || {}; shelf = settings.location || ''; staffBase = s.staff_url || ''; - $('lnk-opac').href = s.opac_url ? s.opac_url + '/' : 'http://' + location.hostname + ':' + s.opac_port + '/'; + // The public catalogue is off by default: only link to it when a public address was configured. + $('lnk-opac').hidden = !s.opac_url; + if (s.opac_url) $('lnk-opac').href = s.opac_url + '/'; $('lnk-staff').href = staffRoot() + '/'; + $('lnk-staff').hidden = viewer; $('lnk-labels').href = staffRoot() + '/cgi-bin/koha/labels/label-edit-range.pl'; if (s.locations && s.locations.length) { const sel = $('shelf'); @@ -504,7 +510,7 @@ sel.onchange = () => { shelf = sel.value; saveSettings({ location: shelf }); $('code').focus(); }; $('shelfbox').hidden = false; } - api('/api/settings').then(applySettings); + if (!viewer) api('/api/settings').then(applySettings); $('earlier').replaceChildren(...s.recent.slice(0, 25).map(e => rowFor(e, false))); showView(); })(); diff --git a/menus/addon_library.sh b/menus/addon_library.sh index 2c9c053..a6c5a88 100644 --- a/menus/addon_library.sh +++ b/menus/addon_library.sh @@ -1,29 +1,30 @@ #!/bin/bash ################################################################################ # menus/addon_library.sh - "Library Catalog" addon: a self-hosted library -# catalogue (Koha) running in Docker, shown on the kiosk as a public -# search terminal. +# catalogue (Koha) running in Docker, with a "Find a book" page on the kiosk. # # What gets installed (all from this checkout's library/ directory, deployed to # $LIBRARY_DIR - see library/README.md for the stack itself): -# - Koha: the catalogue ("OPAC", public search, no login) and the staff -# interface (cataloguing, Z39.50 lookup, label printing). -# - Quick Add: a one-box "scan an ISBN, the book is added" page. +# - Koha: cataloguing, Z39.50 lookup, label printing, people and lending. +# - Quick Add: the "scan an ISBN, the book is added" page, with a Find a book tab. # - MariaDB. # Docker itself is installed on demand (lib/docker.sh). # -# On the kiosk the catalogue is added as one of the Sites (tabs) with -# duration 0 (stay put - it's a search terminal, not a rotating display). -# Staff and Quick Add are meant for the librarian's own device; whether they -# are reachable from the LAN or only from this kiosk is the "Network access" -# choice below. NOTE: Docker publishes ports straight through iptables and -# bypasses ufw, so "this kiosk only" is enforced by binding the published -# ports to 127.0.0.1 (BIND_ADDR in library/.env), not by firewall rules. +# Everything needs a login. The kiosk shows the "Find a book" page signed in as a +# READ-ONLY guest login (Find a book is search only; the guest has no Koha account at +# all, because Koha's own search permission also allows editing label layouts); the page +# list stores that guest login like any other Basic Auth site. The admin login never +# goes into the kiosk's config. The public catalogue (OPAC) is off by default. +# +# Whether other devices on the LAN can reach the library site is the "Network access" +# choice below. NOTE: Docker publishes ports straight through iptables and bypasses +# ufw, so "this kiosk only" is enforced by binding the published ports to 127.0.0.1 +# (BIND_ADDR in library/.env), not by firewall rules. # # Depends on: lib/menu.sh, lib/config.sh, lib/docker.sh being sourced first. ################################################################################ -LIBRARY_CATALOGUE_TAB_NAME="Library catalogue" +LIBRARY_CATALOGUE_TAB_NAME="Find a book" ################################################################################ # Detection / state @@ -42,7 +43,13 @@ library_is_running() { [[ "$(docker_container_state library-koha)" == "running" ]] } +# The page the kiosk shows: Quick Add's Find a book view on the library site. library_catalogue_url() { + echo "http://localhost:$(library_env_get STAFF_PORT)/add/#find" +} + +# Pages from earlier versions of this add-on pointed at the public catalogue. +library_old_catalogue_url() { echo "http://localhost:$(library_env_get OPAC_PORT)/" } @@ -66,19 +73,19 @@ addon_library_status() { health="$(docker_container_health library-koha)" ip="$(get_ip_address)" echo "Library Catalog: ${state}${health:+ ($health)}" - echo " Catalogue: http://localhost:$(library_env_get OPAC_PORT)/ (shown on the kiosk)" + echo " Find a book: $(library_catalogue_url) (shown on the kiosk, as the read-only guest)" if [[ "$(library_env_get BIND_ADDR)" == "127.0.0.1" ]]; then echo " Network: this kiosk only" else - echo " Network: other devices on the LAN can reach it at http://${ip}:$(library_env_get OPAC_PORT)/" + echo " Network: other devices on the LAN can reach the library site at http://${ip}:$(library_env_get STAFF_PORT)/" fi } addon_library_menu_builder() { if library_is_installed; then load_existing_config - local tab_label="Show the catalogue on the kiosk" - library_tab_exists && tab_label="Stop showing the catalogue on the kiosk" + local tab_label="Show Find a book on the kiosk" + library_tab_exists && tab_label="Stop showing Find a book on the kiosk" MENU_LABELS=("Addresses and staff login" "$tab_label" "Network access (this kiosk only / LAN)" "Start" "Stop" "Update" "Uninstall") MENU_HANDLERS=(action_library_show_access action_library_toggle_tab action_library_network action_library_start action_library_stop action_library_update action_library_uninstall) else @@ -111,13 +118,13 @@ library_sync_files() { library_add_tab() { load_existing_config if library_tab_exists; then - log_info "The catalogue is already one of the kiosk's pages." + log_info "Find a book is already one of the kiosk's pages." return 0 fi URLS+=("$(library_catalogue_url)") DURS+=("0") - USERS+=("") - PASSES+=("") + USERS+=("$(library_env_get VIEWER_USER)") # read-only guest, never the admin login + PASSES+=("$(library_env_get VIEWER_PASSWORD)") NAMES+=("$LIBRARY_CATALOGUE_TAB_NAME") save_config } @@ -125,11 +132,12 @@ library_add_tab() { # Drop every page that points at the catalogue's local address. library_remove_tab() { load_existing_config - local url idx + local url old idx url="$(library_catalogue_url)" + old="$(library_old_catalogue_url)" local -a nu=() nd=() nus=() np=() nn=() for idx in "${!URLS[@]}"; do - [[ "${URLS[$idx]}" == "$url" ]] && continue + [[ "${URLS[$idx]}" == "$url" || "${URLS[$idx]}" == "$old" ]] && continue nu+=("${URLS[$idx]}"); nd+=("${DURS[$idx]:-0}"); nus+=("${USERS[$idx]:-}") np+=("${PASSES[$idx]:-}"); nn+=("${NAMES[$idx]:-}") done @@ -199,7 +207,7 @@ action_library_install() { return 1 fi - if ask_yes_no "Show the catalogue on the kiosk screen?" "y"; then + if ask_yes_no "Show the Find a book page on the kiosk screen (signed in as the read-only guest)?" "y"; then library_add_tab fi echo @@ -218,9 +226,11 @@ action_library_show_access() { echo "(Network access is limited to this kiosk.)" fi echo " Your library (sign in here): http://${ip}:$(library_env_get STAFF_PORT)/" - echo " Add books: http://${ip}:$(library_env_get STAFF_PORT)/add/" - echo " Public catalogue (kiosk): http://${ip}:$(library_env_get OPAC_PORT)/" - echo " Login: ${user} / ${pass} (Add books asks once more with the same login)" + echo " Find a book / Add books: http://${ip}:$(library_env_get STAFF_PORT)/add/" + echo " Admin login: ${user} / ${pass}" + echo " Read-only login to share: $(library_env_get VIEWER_USER) / $(library_env_get VIEWER_PASSWORD)" + echo " Give it out with http://${ip}:$(library_env_get STAFF_PORT)/add/ - it opens Find a book: search only," + echo " it cannot add or change anything (and it has no login on the staff site itself)." echo echo " Passwords live in $LIBRARY_DIR/.env (root only)." pause diff --git a/tests/addon_library.test.sh b/tests/addon_library.test.sh index 6f87f97..f16926b 100755 --- a/tests/addon_library.test.sh +++ b/tests/addon_library.test.sh @@ -74,6 +74,8 @@ STAFF_PORT=8081 QUICKADD_PORT=8082 ADMIN_USER=admin ADMIN_PASSWORD=s3cretPASS +VIEWER_USER=guest +VIEWER_PASSWORD=guestPASS BIND_ADDR=${BIND_ADDR:-0.0.0.0} ENV echo running > "$STUB_STATE" @@ -98,7 +100,7 @@ source "$REPO/menus/addon_library.sh" export KIOSK_UNATTENDED=0 docker_ensure_installed() { return 0; } # the stub stands in for a working Docker -tabs() { jq -r '.tabs[] | "\(.url)|\(.duration)|\(.name)"' "$CONFIG_PATH"; } +tabs() { jq -r '.tabs[] | "\(.url)|\(.duration)|\(.name)|\(.username)"' "$CONFIG_PATH"; } # ============================================================================ echo "== lib/docker.sh" @@ -133,8 +135,10 @@ printf 'Smith Family Library\nn\ny\n\n' | action_library_install >"$T/install.ou check "install.sh got the library name" grep -q 'LIBRARY_NAME=Smith Family Library' "$STUB_LOG" check "install.sh got BIND_ADDR=127.0.0.1" grep -q 'BIND_ADDR=127.0.0.1' "$STUB_LOG" check "is_installed is now true" library_is_installed -check "catalogue tab added, duration 0" grep -qx 'http://localhost:8080/|0|Library catalogue' <(tabs) -check "existing tab preserved" grep -qx 'https://example.com|60|Example' <(tabs) +check "Find a book tab added, duration 0" grep -qx 'http://localhost:8081/add/#find|0|Find a book|guest' <(tabs) +check "tab carries the read-only guest password" test "$(jq -r '.tabs[] | select(.name=="Find a book") | .password' "$CONFIG_PATH")" = guestPASS +check "admin password never goes in kiosk config" bash -c "! grep -q s3cretPASS '$CONFIG_PATH'" +check "existing tab preserved" grep -qx 'https://example.com|60|Example|' <(tabs) check "untracked config keys preserved" test "$(jq -r .somethingUntracked "$CONFIG_PATH")" = keep-me addon_library_status > "$T/status.out" check "status shows 'this kiosk only'" grep -q 'this kiosk only' "$T/status.out" @@ -142,21 +146,29 @@ check "status shows 'this kiosk only'" grep -q 'this kiosk only' "$T/sta # ============================================================================ echo "== menu while installed" addon_library_menu_builder -check "offers 'stop showing' (tab exists)" grep -q 'Stop showing the catalogue' <<< "${MENU_LABELS[*]}" +check "offers 'stop showing' (tab exists)" grep -q 'Stop showing Find a book' <<< "${MENU_LABELS[*]}" check "has Update and Uninstall" grep -q 'Update.*Uninstall' <<< "${MENU_LABELS[*]}" echo "== tab toggling is idempotent" library_add_tab >/dev/null 2>&1 -check "adding again does not duplicate" test "$(tabs | grep -c 'localhost:8080')" = 1 +check "adding again does not duplicate" test "$(tabs | grep -c 'localhost:8081/add/#find')" = 1 printf '\n' | action_library_toggle_tab >/dev/null 2>&1 # now removes it -check "toggle removes the catalogue tab" test "$(tabs | grep -c 'localhost:8080')" = 0 -check "toggle keeps the other tab" grep -qx 'https://example.com|60|Example' <(tabs) +check "toggle removes the Find a book tab" test "$(tabs | grep -c 'localhost:8081/add/#find')" = 0 +check "toggle keeps the other tab" grep -qx 'https://example.com|60|Example|' <(tabs) addon_library_menu_builder -check "menu now offers 'Show'" grep -q 'Show the catalogue on the kiosk' <<< "${MENU_LABELS[*]}" +check "menu now offers 'Show'" grep -q 'Show Find a book on the kiosk' <<< "${MENU_LABELS[*]}" printf '\n' | action_library_toggle_tab >/dev/null 2>&1 # and adds it back -check "toggle adds it back" test "$(tabs | grep -c 'localhost:8080')" = 1 +check "toggle adds it back" test "$(tabs | grep -c 'localhost:8081/add/#find')" = 1 # ============================================================================ +echo "== an earlier version's tab (public catalogue address) is cleaned up" +jq '.tabs += [{"url":"http://localhost:8080/","duration":0,"username":"","password":"","name":"Library catalogue"}]' "$CONFIG_PATH" > "$T/cfg.tmp" && cp "$T/cfg.tmp" "$CONFIG_PATH" +check "old catalogue tab present before" test "$(tabs | grep -c '^http://localhost:8080/|')" = 1 +library_remove_tab >/dev/null 2>&1 +check "old catalogue tab removed" test "$(tabs | grep -c '^http://localhost:8080/|')" = 0 +check "other tabs untouched" grep -qx 'https://example.com|60|Example|' <(tabs) +library_add_tab >/dev/null 2>&1 + echo "== network access" : > "$STUB_LOG" printf 'y\n\n' | action_library_network >/dev/null 2>&1 # allow network @@ -168,7 +180,8 @@ check ".env BIND_ADDR back to 127.0.0.1" test "$(library_env_get BIND_ADDR # ============================================================================ echo "== access screen" printf '\n' | action_library_show_access > "$T/access.out" 2>&1 -check "shows the login" grep -q 'admin / s3cretPASS' "$T/access.out" +check "shows the admin login" grep -q 'admin / s3cretPASS' "$T/access.out" +check "shows the read-only login to share" grep -q 'guest / guestPASS' "$T/access.out" check "shows the one library address" grep -q 'sign in here.*:8081/' "$T/access.out" check "shows the add-books path on that address" grep -q ':8081/add/' "$T/access.out" @@ -186,7 +199,7 @@ printf 'n\n\n' | action_library_uninstall >/dev/null 2>&1 check "compose down without -v" grep -qE 'compose .* down$' "$STUB_LOG" check "no volume deletion" bash -c "! grep -q -- ' -v' '$STUB_LOG'" check "install dir and .env kept" test -f "$LIBRARY_DIR/.env" -check "catalogue tab removed" test "$(tabs | grep -c 'localhost:8080')" = 0 +check "catalogue tab removed" test "$(tabs | grep -c 'localhost:8081/add/#find')" = 0 echo "== purge needs the typed confirmation" printf 'y\nnope\n\n' | action_library_uninstall >/dev/null 2>&1 @@ -196,8 +209,8 @@ library_add_tab >/dev/null 2>&1 printf 'y\nDELETE\n\n' | action_library_uninstall >/dev/null 2>&1 check "compose down -v --rmi local" grep -q 'down -v --rmi local' "$STUB_LOG" check "install dir removed" test ! -e "$LIBRARY_DIR" -check "tab removed again" test "$(tabs | grep -c 'localhost:8080')" = 0 -check "other tab still there" grep -qx 'https://example.com|60|Example' <(tabs) +check "tab removed again" test "$(tabs | grep -c 'localhost:8081/add/#find')" = 0 +check "other tab still there" grep -qx 'https://example.com|60|Example|' <(tabs) echo echo "passed: $PASS failed: $FAIL"