From 10e991e158c6ced2e2d505fedcb50e46d1c40199 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 22:45:01 +0000 Subject: [PATCH 1/9] library: staff signed in to Koha open Add books without a second login Quick Add asks Koha (C4::Auth::check_cookie_auth via a secret-guarded svc script) whether the browser's session may edit the catalogue; falls back to its own login otherwise. Cookie-authenticated writes need an X-Quickadd header; the direct port never accepts cookies. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_013wi3ZSot4XbUViwRFgLpRf --- library/README.md | 4 ++ library/docker-compose.yml | 2 + library/install.sh | 3 ++ library/koha/Dockerfile | 4 ++ library/koha/entrypoint.sh | 22 ++++++++-- library/koha/quickadd-session.pl | 44 +++++++++++++++++++ library/quickadd/app.py | 75 ++++++++++++++++++++++++++------ library/quickadd/index.html | 8 ++-- 8 files changed, 142 insertions(+), 20 deletions(-) create mode 100644 library/koha/quickadd-session.pl diff --git a/library/README.md b/library/README.md index c074576..09eb6a9 100644 --- a/library/README.md +++ b/library/README.md @@ -203,3 +203,7 @@ koha/ Dockerfile (Koha from the project's Debian repo), entrypoi servers, label templates/layouts) quickadd/ the scan-to-add service (Python standard library + yaz) ``` + +## Signing in once + +Staff who are signed in to the library site (and allowed to edit the catalogue) open **Add books** without a second login. Koha itself confirms the sign-in; the shared secret is `SSO_SECRET` in `.env`. The read-only `guest` login still uses the Add books prompt, and the direct Add books port never accepts a Koha sign-in. If `SSO_SECRET` is empty, Add books just asks for its own login as before. diff --git a/library/docker-compose.yml b/library/docker-compose.yml index 77009fa..1153cd7 100644 --- a/library/docker-compose.yml +++ b/library/docker-compose.yml @@ -35,6 +35,7 @@ services: environment: TZ: ${TZ:-UTC} KOHA_QUICKADD_URL: http://quickadd:8082 + KOHA_SSO_SECRET: ${SSO_SECRET:-} KOHA_DB_HOST: db KOHA_DB_PASS: ${DB_PASSWORD} KOHA_ADMIN_USER: ${ADMIN_USER:-admin} @@ -60,6 +61,7 @@ services: environment: TZ: ${TZ:-UTC} KOHA_API: http://koha:8081/api/v1 + SSO_SECRET: ${SSO_SECRET:-} ADMIN_USER: ${ADMIN_USER:-admin} ADMIN_PASSWORD: ${ADMIN_PASSWORD} VIEWER_USER: ${VIEWER_USER:-guest} diff --git a/library/install.sh b/library/install.sh index da63d67..0ecded4 100755 --- a/library/install.sh +++ b/library/install.sh @@ -127,6 +127,8 @@ ADMIN_USER=admin ADMIN_PASSWORD=$(rand 16) VIEWER_USER=guest VIEWER_PASSWORD=$(rand 14) +# Lets the library site vouch for a signed-in staff user to the Add books page (no second login). +SSO_SECRET=$(rand 40) DB_ROOT_PASSWORD=$(rand 32) DB_PASSWORD=$(rand 32) KOHA_SUITE=stable @@ -142,6 +144,7 @@ else if ! grep -q '^VIEWER_PASSWORD=' .env; then { echo "VIEWER_USER=guest"; echo "VIEWER_PASSWORD=$(rand 14)"; } >> .env fi + grep -q '^SSO_SECRET=' .env || echo "SSO_SECRET=$(rand 40)" >> .env # 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 diff --git a/library/koha/Dockerfile b/library/koha/Dockerfile index 5dda088..03d9005 100644 --- a/library/koha/Dockerfile +++ b/library/koha/Dockerfile @@ -39,6 +39,10 @@ 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 +# The endpoint Quick Add uses to recognise a signed-in staff user (see the file header). +COPY quickadd-session.pl /usr/share/koha/intranet/cgi-bin/svc/quickadd-session +RUN chmod 0755 /usr/share/koha/intranet/cgi-bin/svc/quickadd-session + 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/entrypoint.sh b/library/koha/entrypoint.sh index c1532c1..6d7bbae 100755 --- a/library/koha/entrypoint.sh +++ b/library/koha/entrypoint.sh @@ -60,22 +60,36 @@ if [ ! -d "/etc/koha/sites/$INST" ]; then # (The staff and public catalogue sites cannot share one address: Koha gives both the # same URL shapes. The staff site is the natural home for a home library.) if [ -n "${KOHA_QUICKADD_URL:-}" ]; then + # Single sign-on for staff: when KOHA_SSO_SECRET is set, this web server tells Quick Add (via a + # secret header nobody outside can forge: it is overwritten here on every request) that the + # request came through the staff site, and Quick Add then asks Koha whether the browser's login + # cookie belongs to a staff user who may edit the catalogue. Without the secret, or if anything + # about that fails, Quick Add just asks for its own login as before. + SSO_BLOCK="" + if [ -n "${KOHA_SSO_SECRET:-}" ]; then + install -m 0640 -o root -g "$INST-koha" /dev/null /etc/koha/quickadd-sso.secret + printf '%s' "$KOHA_SSO_SECRET" > /etc/koha/quickadd-sso.secret + SSO_BLOCK=" + + RequestHeader set X-Quickadd-Proxy \"$KOHA_SSO_SECRET\" + + " + fi cat > /etc/koha/apache-quickadd.conf << APACHE RedirectMatch ^/add\$ /add/ ProxyPass /add/ ${KOHA_QUICKADD_URL%/}/ ProxyPassReverse /add/ ${KOHA_QUICKADD_URL%/}/ +${SSO_BLOCK} APACHE sed -i 's|^\(\s*\)Include /etc/koha/apache-shared-intranet.conf|&\n\1Include /etc/koha/apache-quickadd.conf|' "/etc/apache2/sites-available/$INST.conf" fi - # Run Apache itself as the instance user instead of mpm_itk's per-vhost - # uid switching (which needs extra capabilities and is pointless here: - # Plack does the real work as that user already). # koha-create insists on mpm_itk being enabled, but inside a container it can # not switch users (initgroups: Operation not permitted -> HTTP 500 on every - # page). Now that the instance exists, swap to plain prefork. + # page). Now that the instance exists, swap to plain prefork; Apache runs as the + # instance user (Plack does the real work as that user already). rm -f /etc/apache2/mods-enabled/mpm_itk.* a2enmod mpm_prefork >/dev/null 2>&1 || true sed -i '/AssignUserID/d' "$SITE" diff --git a/library/koha/quickadd-session.pl b/library/koha/quickadd-session.pl new file mode 100644 index 0000000..e60a487 --- /dev/null +++ b/library/koha/quickadd-session.pl @@ -0,0 +1,44 @@ +#!/usr/bin/perl +# Installed as .../intranet/cgi-bin/svc/quickadd-session (URL /cgi-bin/koha/svc/quickadd-session). +# +# Lets the Add books page (Quick Add) ask: "is this browser signed in to the staff site as +# someone allowed to edit the catalogue?" so staff do not have to log in a second time. +# +# It wraps Koha's own C4::Auth::check_cookie_auth (documented for exactly this: services that +# are invoked by pages authenticated in the usual way) and only answers callers that present a +# shared secret, so it cannot be used to probe sessions from outside. The browser's address is +# passed in explicitly, so Koha's "session is tied to the IP that logged in" rule keeps working. +use Modern::Perl; +use CGI qw( -utf8 ); +use JSON qw( encode_json ); +use Digest::SHA qw( sha256_hex ); +use C4::Auth qw( check_cookie_auth ); + +my $q = CGI->new; + +# Takes the CGI object as an argument: under Plack this file runs wrapped in a sub, where a named +# sub would otherwise see a stale copy of a file-level variable. +sub reply { + my ( $q, $status, $data ) = @_; + print $q->header( -type => 'application/json', -status => $status, -charset => 'utf-8', -cache_control => 'no-store' ), + encode_json($data); + exit; +} + +# 1. The caller must know the shared secret (a file only the Koha user and root can read). +my $secret = ''; +if ( open my $fh, '<', '/etc/koha/quickadd-sso.secret' ) { $secret = <$fh> // ''; $secret =~ s/\s+$//; close $fh } +my $given = $ENV{HTTP_X_QUICKADD_SECRET} // ''; +reply( $q, '403 Forbidden', { status => 'forbidden' } ) + unless length($secret) >= 16 && sha256_hex($given) eq sha256_hex($secret); + +# 2. Plausible-looking inputs only. +my $sid = $q->param('sid') // ''; +my $ip = $q->param('ip') // ''; +reply( $q, '200 OK', { status => 'failed' } ) + unless $sid =~ /\A[A-Za-z0-9]{16,64}\z/ && $ip =~ /\A[0-9A-Fa-f:.]{3,45}\z/; + +# 3. Ask Koha. Staff who may edit the catalogue (superlibrarians always may) are let in. +my ( $status, $session ) = check_cookie_auth( $sid, { editcatalogue => 'edit_catalogue' }, { remote_addr => $ip } ); +reply( $q, '200 OK', { status => 'ok', user => scalar $session->param('id') } ) if $status eq 'ok' && $session; +reply( $q, '200 OK', { status => $status || 'failed' } ); diff --git a/library/quickadd/app.py b/library/quickadd/app.py index f82a644..3d4ad68 100644 --- a/library/quickadd/app.py +++ b/library/quickadd/app.py @@ -28,6 +28,11 @@ 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. +# Single sign-on: requests that reach us through the staff site's web server carry this secret in +# X-Quickadd-Proxy; we then ask Koha whether the browser's login cookie is a staff user who may edit +# the catalogue. Unset = feature off (only the Basic logins below work). +SSO_SECRET = os.environ.get("SSO_SECRET", "") +KOHA_SESSION_URL = KOHA_API.rsplit("/api/v1", 1)[0] + "/cgi-bin/koha/svc/quickadd-session" VIEWER_USER = os.environ.get("VIEWER_USER", "") VIEWER_PASSWORD = os.environ.get("VIEWER_PASSWORD", "") LIBRARY = os.environ.get("KOHA_LIBRARY", "HOME") @@ -782,6 +787,33 @@ def decode_photo(data, kind): INDEX = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html"), "rb").read() +_SESSION_CACHE = {} + + +def koha_session_user(sid, ip): + """Ask Koha whether this staff session may edit the catalogue. Returns the user name or None. + Any problem (Koha starting, endpoint missing, odd reply) reads as 'not signed in', which just + means Quick Add asks for its own login.""" + key, now = (sid, ip), time.time() + hit = _SESSION_CACHE.get(key) + if hit and hit[0] > now: + return hit[1] + user = None + try: + req = urllib.request.Request(KOHA_SESSION_URL + "?" + urllib.parse.urlencode({"sid": sid, "ip": ip}), + headers={"X-Quickadd-Secret": SSO_SECRET}) + with urllib.request.urlopen(req, timeout=4) as r: + data = json.load(r) + if data.get("status") == "ok": + user = data.get("user") or "staff" + except Exception: + user = None + if len(_SESSION_CACHE) > 500: + _SESSION_CACHE.clear() + _SESSION_CACHE[key] = (now + (20 if user else 5), user) # short: logging out takes effect quickly + return user + + class Handler(BaseHTTPRequestHandler): server_version = "QuickAdd/1.0" @@ -806,22 +838,36 @@ class Handler(BaseHTTPRequestHandler): self.wfile.write(body) def _role(self): - """'admin', 'viewer' (read-only guest) or None.""" + """'admin', 'viewer' (read-only guest) or None. Sets self.via to 'koha' for a staff-site login.""" + self.via = None if not PASSWORD: return None header = self.headers.get("Authorization", "") - 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 + if header.startswith("Basic "): + 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 + # No Basic login: accept a staff-site login, but only for requests that came through the staff + # site's web server (it overwrites X-Quickadd-Proxy with the shared secret on every request). + if SSO_SECRET and hmac.compare_digest(self.headers.get("X-Quickadd-Proxy", "").encode(), SSO_SECRET.encode()): + sid = "" + for part in self.headers.get("Cookie", "").split(";"): + k, _, v = part.strip().partition("=") + if k == "CGISESSID": + sid = v + # the browser's address as the web server saw it: the LAST entry (the one it appended) + ip = self.headers.get("X-Forwarded-For", "").split(",")[-1].strip() + if sid and ip and koha_session_user(sid, ip): + self.via = "koha" + return "admin" + return None def _guard(self, need_admin=True): role = self._role() @@ -832,6 +878,9 @@ class Handler(BaseHTTPRequestHandler): if need_admin and role != "admin": self._send(403, {"status": "error", "message": "This login can search but not change anything."}) return False + if self.via == "koha" and self.command == "POST" and self.headers.get("X-Quickadd") != "1": + self._send(403, {"status": "error", "message": "Missing request header."}) + return False self.role = role return True diff --git a/library/quickadd/index.html b/library/quickadd/index.html index 5d9306c..b813688 100644 --- a/library/quickadd/index.html +++ b/library/quickadd/index.html @@ -211,8 +211,10 @@ }; async function api(path, body) { - const r = await fetch(path.replace(/^\//, ''), body ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body) } : {}); + // X-Quickadd marks our own requests: a staff-site login (cookie) is only accepted for changes + // that carry it, which a page on another website cannot add (cross-site request forgery). + const r = await fetch(path.replace(/^\//, ''), body ? { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Quickadd': '1' }, + body: JSON.stringify(body) } : { headers: { 'X-Quickadd': '1' } }); return r.json(); } @@ -329,7 +331,7 @@ $('status').textContent = 'Reading the barcode…'; try { const blob = await shrink(f); - const r = await fetch('api/scan?kind=' + kind, { method: 'POST', headers: { 'Content-Type': 'image/jpeg' }, body: blob }); + const r = await fetch('api/scan?kind=' + kind, { method: 'POST', headers: { 'Content-Type': 'image/jpeg', 'X-Quickadd': '1' }, body: blob }); resolve(r.ok ? (await r.json()).codes : []); } catch (e) { resolve([]); } }; From d01919e2ac0f190bde22155c7717cbe8330912a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 23:56:08 +0000 Subject: [PATCH 2/9] library: Library home page (cover grid, filters, search) and book page with shelf/call-number editing Quick Add becomes the everyday interface: Library grid with shelf/format/status filters, a book page (copies, status, edit shelf and call number, add/remove copy, delete), read-only for the guest. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_013wi3ZSot4XbUViwRFgLpRf --- .../quickadd/__pycache__/app.cpython-313.pyc | Bin 0 -> 75979 bytes library/quickadd/app.py | 180 +++++++++++++- library/quickadd/index.html | 223 +++++++++++++++--- 3 files changed, 364 insertions(+), 39 deletions(-) create mode 100644 library/quickadd/__pycache__/app.cpython-313.pyc diff --git a/library/quickadd/__pycache__/app.cpython-313.pyc b/library/quickadd/__pycache__/app.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b023abef3b7cbeadbf7d438745faca060f0df80 GIT binary patch literal 75979 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 PAGE_SIZE + books = books[:PAGE_SIZE] + if not books: + return {"books": [], "page": page, "more": False} + ids = [b["biblio_id"] for b in books] + items = koha("GET", "/items", query={"q": json.dumps({"biblio_id": ids}), "_per_page": "1000"}) or [] + where = {l["value"]: l["label"] for l in get_locations()} + return {"books": [_shape_book(b, items, where) for b in books], "page": page, "more": more} + + +def _checkout_info(item_ids, names): + """item_id -> {who, due} for items currently out (staff only).""" + if not item_ids: + return {} + cos = koha("GET", "/checkouts", query={"q": json.dumps({"item_id": item_ids}), "_per_page": "100"}) or [] + out = {} + for c in cos: + who = "" + if names: + try: + p = koha("GET", "/patrons/%s" % c["patron_id"]) + who = ("%s %s" % (p.get("firstname") or "", p.get("surname") or "")).strip() + except KohaError: + pass + out[c["item_id"]] = {"who": who, "patron_id": c["patron_id"], "due": (c.get("due_date") or "")[:10], + "checkout_id": c["checkout_id"]} + return out + + +def op_book(biblio_id, staff): + b = koha("GET", "/biblios/%d" % biblio_id) + items = koha("GET", "/biblios/%d/items" % biblio_id) or [] + where = {l["value"]: l["label"] for l in get_locations()} + out = _checkout_info([i["item_id"] for i in items if item_status(i) == "Checked out"], staff) + copies = [] + for i in items: + c = {"item_id": i["item_id"], "barcode": i.get("external_id") or "", "callnumber": i.get("callnumber") or "", + "shelf": i.get("location") or "", "shelf_label": where.get(i.get("location"), ""), + "status": item_status(i), "type": i.get("item_type_id") or ""} + if staff and c["status"] == "Checked out": + c["out"] = out.get(i["item_id"]) + copies.append(c) + return {"id": b["biblio_id"], "title": (b.get("title") or "").rstrip(" /:"), "subtitle": b.get("subtitle") or "", + "author": b.get("author") or "", "year": b.get("copyright_date") or b.get("publication_year") or "", + "isbn": b.get("isbn") or "", "publisher": b.get("publisher") or "", "pages": b.get("pages") or "", + "edition": b.get("edition_statement") or "", "summary": b.get("abstract") or b.get("notes") or "", + "series": b.get("series_title") or "", "type": b.get("item_type") or "", "copies": copies, + "shelves": get_locations()} + + +def op_item_update(payload): + item_id = payload.get("item_id") + if not isinstance(item_id, int): + return 400, {"status": "invalid"} + with LOCK: + it = koha("GET", "/items/%d" % item_id) + if "shelf" in payload: + it["location"] = valid_location(payload.get("shelf")) or None + if "callnumber" in payload: + it["callnumber"] = (str(payload.get("callnumber") or "").strip()[:80]) or None + # Koha's update route rejects the computed fields the GET adds. + for k in [k for k in it if k.startswith("effective_")] + [ + "checkouts_count", "holds_count", "renewals_count", "localuse", "timestamp", "call_number_sort", + "bookable", "extended_subfields"]: + it.pop(k, None) + koha("PUT", "/biblios/%d/items/%d" % (it["biblio_id"], item_id), it) + return 200, {"status": "saved"} + + +def op_item_delete(payload): + item_id, biblio_id = payload.get("item_id"), payload.get("biblio_id") + if not isinstance(item_id, int) or not isinstance(biblio_id, int): + return 400, {"status": "invalid"} + return op_undo({"item_id": item_id, "biblio_id": biblio_id}) + + +def op_book_delete(payload): + biblio_id = payload.get("biblio_id") + if not isinstance(biblio_id, int): + return 400, {"status": "invalid"} + with LOCK: + items = koha("GET", "/biblios/%d/items" % biblio_id) or [] + if any(item_status(i) == "Checked out" for i in items): + return 409, {"status": "error", "message": "A copy is checked out - return it first."} + for i in items: + koha("DELETE", "/items/%d" % i["item_id"]) + forget(i["item_id"]) + koha("DELETE", "/biblios/%d" % biblio_id) + return 200, {"status": "deleted"} + + +def op_item_copy(payload): + """Another copy of a book already in the library (new barcode per the barcode setting).""" + biblio_id = payload.get("biblio_id") + if not isinstance(biblio_id, int): + return 400, {"status": "invalid"} + with LOCK: + b = koha("GET", "/biblios/%d" % biblio_id) + items = koha("GET", "/biblios/%d/items" % biblio_id) or [] + base = items[0] if items else {} + mode = load_settings()["barcode_mode"] + if mode == "scan": + mode = "auto" # no scanner step here: number it, or leave it blank in 'none' mode + item = add_item(biblio_id, base.get("item_type_id") or valid_type(b.get("item_type")), base.get("callnumber"), + valid_location(payload.get("shelf")) or base.get("location"), mode) + return 200, {"status": "added", "item_id": item["item_id"], "barcode": item.get("external_id") or ""} + + # ------------------------------------------------------------ photo scanning MAX_IMAGE = 12 * 1024 * 1024 ZBAR_TIMEOUT = 20 @@ -888,7 +1049,7 @@ class Handler(BaseHTTPRequestHandler): if self.path == "/healthz": return self._send(200, b"ok", "text/plain") path = self.path.split("?")[0] - if not self._guard(need_admin=path not in ("/", "/api/state", "/api/search")): + if not self._guard(need_admin=path not in ("/", "/api/state", "/api/search", "/api/library") and not path.startswith("/api/book/")): return if path == "/": return self._send(200, INDEX, "text/html; charset=utf-8") @@ -902,6 +1063,19 @@ class Handler(BaseHTTPRequestHandler): "sources": [s.split(":", 1)[0] if s.startswith("z3950") else s for s in SOURCES]}) if path == "/api/settings": return self._send(200, settings_view()) + if path == "/api/library" or path.startswith("/api/book/"): + try: + if path == "/api/library": + return self._send(200, dict(op_library(urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)), + shelves=get_locations(), types=get_item_types())) + m = re.match(r"^/api/book/(\d{1,9})$", path) + if not m: + return self._send(404, {"error": "not found"}) + return self._send(200, op_book(int(m.group(1)), self.role == "admin")) + except KohaError as e: + return self._send(404 if e.status == 404 else 502, {"status": "error", "message": + "Koha is not answering yet - try again in a minute." if e.status == 503 + else "Not found." if e.status == 404 else "Failed: %s" % e.body[:120]}) if path == "/api/search": qs = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query) try: @@ -931,7 +1105,9 @@ class Handler(BaseHTTPRequestHandler): payload = json.loads(self.rfile.read(length) or b"{}") ops = {"/api/add": op_add, "/api/manual": op_manual, "/api/undo": op_undo, "/api/finish": op_finish, "/api/cancel": op_cancel, "/api/settings": op_settings, - "/api/barcodes/reserve": op_reserve, "/api/barcodes/assign": op_assign} + "/api/barcodes/reserve": op_reserve, "/api/barcodes/assign": op_assign, + "/api/item/update": op_item_update, "/api/item/delete": op_item_delete, + "/api/item/copy": op_item_copy, "/api/book/delete": op_book_delete} fn = ops.get(self.path) if not fn: return self._send(404, {"error": "not found"}) diff --git a/library/quickadd/index.html b/library/quickadd/index.html index b813688..5a73fb9 100644 --- a/library/quickadd/index.html +++ b/library/quickadd/index.html @@ -3,7 +3,7 @@ -Add to library +Library
-

Add to library

+

Library

-
+