diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d26f654..f9406449 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,39 @@ ADR-007 and ADR-002. ### Added +- **USB kiosk: check a drive out and back in without a login.** The people who + actually take USB drives are standing at the cabinet, and most of them have no + ShopDB account - so check-out and check-in were gated behind a login that was + not doing much anyway, since both modes already attribute the action to a + scanned BADGE rather than to the signed-in user. `/usb-kiosk` is a full-screen + touch screen modelled on the parts kiosk, and works against the self-hosted + tables or an existing `cmmc_usb` database. + + It meets the same bar as the parts kiosk: it can move a device between + checked-out and checked-in and nothing else - no create, edit or retire - and + it does not exist at all until a site sets `usb_kiosk_enabled`. An + unauthenticated write is a decision about the door. + + The drives carry no barcode, so unlike the parts kiosk there is nothing to + present and the screen has to offer a LIST. That widens what a logged-out + screen exposes, so the list is not anonymous: an SSO comes first and has to + resolve to a real person before any drive is named - the same bar taking one + already had to clear. The main screen shows the caller's own drives, what is + free, and a COUNT of everything else; the holders of other people's drives + appear only behind a deliberate second tap, so a passer-by cannot read the + whole cabinet off a wall panel. + + A badge that resolves to no employee is REFUSED, unlike the signed-in route + which creates the user row: a checkout nobody can trace to a person defeats + the point, and at a kiosk there is no operator standing there to vouch for it. + + Returning somebody else's drive asks first, then records the badge that was + scanned - so the log says who actually handed it back, which is the honest + record and needs no change to a schema this app does not own. Refusing a + mismatch outright is available as a setting, but is not the default: a drive + left behind by somebody on leave would then be unreturnable at the kiosk, + which just moves the problem to a person with a login. + - **Audit log retention.** The audit log is append-only and nothing ever removed a row, so it grew for as long as a site ran - here to the point where the audit page was tens of thousands of pages deep and therefore useless, because diff --git a/docs/PLUGINS.md b/docs/PLUGINS.md index 03aeb1ad..5e775c47 100644 --- a/docs/PLUGINS.md +++ b/docs/PLUGINS.md @@ -12,7 +12,7 @@ These plugins are in `plugins/` in this repo. Enable per site with `flask plugin | `computers` | Shop-floor PCs and engineering workstations | Fed by the PXE pipeline collector per [ADR-006](adr/ADR-006-collector-contract.md). | | `printers` | Network and shop-floor printers | Public installer map page + fleet install contract (`/api/printers/install-list`, `/pc-default`, `/install-batch`; see [PRINTER-INSTALLER.md](PRINTER-INSTALLER.md)). Optional Zabbix integration for supply tracking. Legacy `PrinterData` retiring per ADR-001. | | `network` | Switches, routers, access points, IDFs as locations | Asset-only; cleanest of the bundled set. | -| `usb` | USB devices issued to shop-floor users | Lightweight checkout / check-in. | +| `usb` | USB devices issued to shop-floor users | Lightweight checkout / check-in, self-hosted tables or an existing `cmmc_usb` database. Optional logged-out kiosk (`/usb-kiosk`, off by default) so somebody at the drive cabinet can take and return a drive without a ShopDB login, attributed to a scanned badge. | | `notifications` | Shop-floor notifications, recognitions, kiosk feed | Used by `ShopfloorDashboard.vue`. | | `measuringtools` | Metrology and inspection instruments: calipers, micrometers, thread/bore/height gages, indicators | Per [ADR-005](adr/ADR-005-equipment-vs-measuringtools.md). Calibration lifecycle with derived status. First plugin built on the matured scaffold; its walkthrough is [PLUGIN-GUIDE.md](PLUGIN-GUIDE.md). Ships `default_enabled: false`. | | `employees` | Read-only employee directory lookup | Backed by a separate HR database. Ships `default_enabled: false`. | diff --git a/docs/PROJECT-MAP.md b/docs/PROJECT-MAP.md index d9eed65c..07ad9695 100644 --- a/docs/PROJECT-MAP.md +++ b/docs/PROJECT-MAP.md @@ -88,6 +88,6 @@ Manifest-less directories under `plugins/` are core frontend surface and always ## Size -- test functions defined: **1333** (parametrised cases collect higher) -- documented API paths: **299** (`docs/openapi.json`, regenerate with `scripts/gen_openapi.py`) +- test functions defined: **1355** (parametrised cases collect higher) +- documented API paths: **304** (`docs/openapi.json`, regenerate with `scripts/gen_openapi.py`) diff --git a/docs/api-inventory.json b/docs/api-inventory.json index 9e2d28af..c2e1946b 100644 --- a/docs/api-inventory.json +++ b/docs/api-inventory.json @@ -2802,6 +2802,46 @@ "auth": "jwt-optional", "params": "query: badge (filter by badge_number)", "example": "curl 'http://localhost:5001/api/usb/checkouts/active?badge=212345678' -H \"Authorization: Bearer $JWT\"" + }, + { + "method": "GET", + "path": "/api/usb/kiosk/device/", + "auth": "none (kiosk; requires usb_kiosk_enabled)", + "params": "path: device_id (the code on the drive label)", + "purpose": "What the logged-out kiosk shows after a drive is scanned: description, checked-in/out status, locker, and the NAME of the current holder (needed for the return-for-someone-else confirm; the holder's badge is never returned). 404 when the kiosk setting is off, so a disabled kiosk does not advertise the route", + "example": "curl http://localhost:5001/api/usb/kiosk/device/USB-1" + }, + { + "method": "POST", + "path": "/api/usb/kiosk/checkout", + "auth": "none (kiosk; requires usb_kiosk_enabled)", + "params": "body: device_id, badge", + "purpose": "Check a drive out at the kiosk, attributed to the scanned badge. A badge that resolves to no employee is refused with 422 (unlike the signed-in route, which creates the user row). 409 when the drive is already out", + "example": "curl -X POST http://localhost:5001/api/usb/kiosk/checkout -H 'Content-Type: application/json' -d '{\"device_id\":\"USB-1\",\"badge\":\"270015376\"}'" + }, + { + "method": "POST", + "path": "/api/usb/kiosk/checkin", + "auth": "none (kiosk; requires usb_kiosk_enabled)", + "params": "body: device_id, badge, forsomeoneelse (optional bool)", + "purpose": "Return a drive at the kiosk. Holder by default: a badge that does not match the current holder gets 409 with details.needsconfirm and the holder's name, and the caller re-posts with forsomeoneelse=true. The badge RECORDED is the one scanned, so the log shows who handed it back. Set usb_kiosk_allow_return_for_others=false to refuse a mismatch with 403 instead", + "example": "curl -X POST http://localhost:5001/api/usb/kiosk/checkin -H 'Content-Type: application/json' -d '{\"device_id\":\"USB-1\",\"badge\":\"270015376\"}'" + }, + { + "method": "POST", + "path": "/api/usb/kiosk/session", + "auth": "none (kiosk; requires usb_kiosk_enabled)", + "params": "body: badge", + "purpose": "What one badge may do at the kiosk: the caller's own drives (to return), the drives that are free (to take), and a COUNT of everything else. The drives carry no barcode so the kiosk must offer a list; requiring a badge that resolves to a real person keeps that list from being anonymous, and a POST keeps the badge out of proxy access logs. 422 for an unresolvable badge", + "example": "curl -X POST http://localhost:5001/api/usb/kiosk/session -H 'Content-Type: application/json' -d '{\"badge\":\"270015376\"}'" + }, + { + "method": "POST", + "path": "/api/usb/kiosk/others", + "auth": "none (kiosk; requires usb_kiosk_enabled)", + "params": "body: badge", + "purpose": "Drives checked out to OTHER people, with holder names, so somebody can return one left behind. Behind its own step rather than on the main kiosk screen, and 404 when usb_kiosk_allow_return_for_others is off - there is no point naming holders of drives the kiosk will not let anyone return", + "example": "curl -X POST http://localhost:5001/api/usb/kiosk/others -H 'Content-Type: application/json' -d '{\"badge\":\"270015376\"}'" } ] }, diff --git a/docs/openapi.json b/docs/openapi.json index 0b2b9ae6..2724a888 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -16272,6 +16272,207 @@ } } }, + "/api/usb/kiosk/device/{device_id}": { + "get": { + "tags": [ + "plugin-usb" + ], + "summary": "What the logged-out kiosk shows after a drive is scanned: description, checked-in/out status, locker, and the NAME of...", + "description": "What the logged-out kiosk shows after a drive is scanned: description, checked-in/out status, locker, and the NAME of the current holder (needed for the return-for-someone-else confirm; the holder's badge is never returned). 404 when the kiosk setting is off, so a disabled kiosk does not advertise the route\n\n**Auth:** none (kiosk; requires usb_kiosk_enabled)\n\n**Params:** path: device_id (the code on the drive label)\n\n**Example:**\n```\ncurl http://localhost:5001/api/usb/kiosk/device/USB-1\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "device_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/usb/kiosk/checkout": { + "post": { + "tags": [ + "plugin-usb" + ], + "summary": "Check a drive out at the kiosk, attributed to the scanned badge. A badge that resolves to no employee is refused with...", + "description": "Check a drive out at the kiosk, attributed to the scanned badge. A badge that resolves to no employee is refused with 422 (unlike the signed-in route, which creates the user row). 409 when the drive is already out\n\n**Auth:** none (kiosk; requires usb_kiosk_enabled)\n\n**Params:** body: device_id, badge\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/usb/kiosk/checkout -H 'Content-Type: application/json' -d '{\"device_id\":\"USB-1\",\"badge\":\"270015376\"}'\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: device_id, badge" + } + } + } + } + } + }, + "/api/usb/kiosk/checkin": { + "post": { + "tags": [ + "plugin-usb" + ], + "summary": "Return a drive at the kiosk. Holder by default: a badge that does not match the current holder gets 409 with...", + "description": "Return a drive at the kiosk. Holder by default: a badge that does not match the current holder gets 409 with details.needsconfirm and the holder's name, and the caller re-posts with forsomeoneelse=true. The badge RECORDED is the one scanned, so the log shows who handed it back. Set usb_kiosk_allow_return_for_others=false to refuse a mismatch with 403 instead\n\n**Auth:** none (kiosk; requires usb_kiosk_enabled)\n\n**Params:** body: device_id, badge, forsomeoneelse (optional bool)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/usb/kiosk/checkin -H 'Content-Type: application/json' -d '{\"device_id\":\"USB-1\",\"badge\":\"270015376\"}'\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: device_id, badge, forsomeoneelse (optional bool)" + } + } + } + } + } + }, + "/api/usb/kiosk/session": { + "post": { + "tags": [ + "plugin-usb" + ], + "summary": "What one badge may do at the kiosk: the caller's own drives (to return), the drives that are free (to take), and a...", + "description": "What one badge may do at the kiosk: the caller's own drives (to return), the drives that are free (to take), and a COUNT of everything else. The drives carry no barcode so the kiosk must offer a list; requiring a badge that resolves to a real person keeps that list from being anonymous, and a POST keeps the badge out of proxy access logs. 422 for an unresolvable badge\n\n**Auth:** none (kiosk; requires usb_kiosk_enabled)\n\n**Params:** body: badge\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/usb/kiosk/session -H 'Content-Type: application/json' -d '{\"badge\":\"270015376\"}'\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: badge" + } + } + } + } + } + }, + "/api/usb/kiosk/others": { + "post": { + "tags": [ + "plugin-usb" + ], + "summary": "Drives checked out to OTHER people, with holder names, so somebody can return one left behind. Behind its own step...", + "description": "Drives checked out to OTHER people, with holder names, so somebody can return one left behind. Behind its own step rather than on the main kiosk screen, and 404 when usb_kiosk_allow_return_for_others is off - there is no point naming holders of drives the kiosk will not let anyone return\n\n**Auth:** none (kiosk; requires usb_kiosk_enabled)\n\n**Params:** body: badge\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/usb/kiosk/others -H 'Content-Type: application/json' -d '{\"badge\":\"270015376\"}'\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: badge" + } + } + } + } + } + }, "/api/printers/types": { "get": { "tags": [