From 65b0197b2b1885348f1b74af3d414a5919a57742 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Fri, 10 Jul 2026 08:19:26 -0400 Subject: [PATCH] Document external-DB schema contracts for employees + USB plugins Every site's HR directory and USB check-in/out databases may use a different schema, so document exactly what each plugin queries and how to adapt. - plugins/employees/README.md: required employees table columns (SSO, First_Name, Last_Name, Team, Role, Picture), the queries run, photo handling, and a CREATE VIEW recipe to map a different site schema without code changes. - plugins/usb/README.md: cmmc_usb devices / checkinoutlog / users columns, read-write ops, the employee-directory dependency, and a view recipe. - USB plugin gains get_config_schema() (cmmc_usb_db_host/name/user + password); cmmc_usb_connection reads host/name/user settings-first (env fallback), the password stays env-only - matching the employees plugin. - Config-field help points at the READMEs. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/employees/README.md | 94 +++++++++++++++++++++++++++++++++++ plugins/employees/plugin.py | 5 +- plugins/usb/README.md | 98 +++++++++++++++++++++++++++++++++++++ plugins/usb/plugin.py | 19 ++++++- shopdb/utils/cmmc_usb_db.py | 22 +++++++-- 5 files changed, 233 insertions(+), 5 deletions(-) create mode 100644 plugins/employees/README.md create mode 100644 plugins/usb/README.md diff --git a/plugins/employees/README.md b/plugins/employees/README.md new file mode 100644 index 0000000..4d27ab7 --- /dev/null +++ b/plugins/employees/README.md @@ -0,0 +1,94 @@ +# Employees plugin - directory database contract + +Read-only lookups against a **separate** employee/HR directory database. Powers: + +- Employee search (add people to a notification, browse the directory) +- Single + batch SSO lookup +- Recognition notifications (name + photo on the shopfloor / lobby displays) + +The plugin never writes to this database. Use a read-only account. + +## Connection + +Credentials resolve **settings-first, then environment**, except the password +which is env-only (never stored in the app database). + +| Field | Setting key (editable in the setup wizard) | Env var (fallback) | +| -------- | ------------------------------------------ | ---------------------- | +| Host | `employee_db_host` | `EMPLOYEE_DB_HOST` | +| Database | `employee_db_name` | `EMPLOYEE_DB_NAME` | +| User | `employee_db_user` | `EMPLOYEE_DB_USER` | +| Password | (not stored) | `EMPLOYEE_DB_PASSWORD` | + +Set the password in `.env`; the setup wizard's Features step shows the exact +line to paste. Engine is MySQL/MariaDB (pymysql). + +## Required schema + +The plugin runs these queries verbatim, so a site's database must expose a +table (or **view** - see below) named `employees` with these columns: + +| Column | Type | Notes | +| ------------ | ------------------- | ------------------------------------------------- | +| `SSO` | INT | Unique person id. Lookups require it to be numeric | +| `First_Name` | VARCHAR | Displayed as the given name | +| `Last_Name` | VARCHAR | Displayed as the surname; sort key | +| `Team` | VARCHAR | Team / group label | +| `Role` | VARCHAR | Job title / role | +| `Picture` | VARCHAR | **Image filename** (see Photos) | + +Queries actually executed: + +```sql +-- search +SELECT SSO, First_Name, Last_Name, Team, Role, Picture +FROM employees +WHERE First_Name LIKE %s OR Last_Name LIKE %s OR CAST(SSO AS CHAR) LIKE %s +ORDER BY Last_Name, First_Name LIMIT %s; + +-- single / batch +SELECT SSO, First_Name, Last_Name, Team, Role, Picture FROM employees WHERE SSO = %s; +SELECT SSO, First_Name, Last_Name, Team, Role, Picture FROM employees WHERE SSO IN (...); +``` + +Rows are returned to the API **with these exact column names**. The frontend +(EmployeeSearch, NotificationForm, EmployeeDetail, ShopfloorDashboard) reads +`SSO`, `First_Name`, `Last_Name`, `Team`, `Role`, `Picture` as-is - do not +rename them in the response. + +## Photos + +`Picture` holds an **image filename** (e.g. `123456.jpg`), not a path or blob. +The app renders it as `/static/employees/`, so the image files must +live in the app's `static/employees/` directory. Leave `Picture` empty/NULL for +people with no photo; the UI falls back to initials. + +## Adapting a different site schema (recommended: a view) + +Sites whose HR/directory database uses different table or column names should +**not** be forced to rename anything. Instead, create a read-only **view** +named `employees` that maps local columns to the names above: + +```sql +CREATE VIEW employees AS +SELECT + person_id AS SSO, + given_name AS First_Name, + surname AS Last_Name, + department AS Team, + job_title AS Role, + photo_filename AS Picture +FROM hr_people +WHERE active = 1; +``` + +Grant the app's read-only user `SELECT` on the view. No app code changes - +point `employee_db_*` at that database and the plugin works. + +Notes: +- `SSO` must be numeric (single/batch lookup validate `isdigit()`). +- Column-name case follows your database's identifier casing; match the names + above exactly on case-sensitive platforms. +- If the directory is unreachable or the `employees` object is missing, lookups + return a 500 and the rest of the app keeps working (the feature degrades, it + does not crash the app). diff --git a/plugins/employees/plugin.py b/plugins/employees/plugin.py index c0fa30f..e298802 100644 --- a/plugins/employees/plugin.py +++ b/plugins/employees/plugin.py @@ -63,7 +63,10 @@ class EmployeesPlugin(BasePlugin): wizard can edit; the password stays in .env (emitted, not stored).""" return [ {'key': 'employee_db_host', 'label': 'Employee DB host', 'type': 'text', - 'secret': False, 'default': 'localhost'}, + 'secret': False, 'default': 'localhost', + 'help': 'This DB must expose an "employees" table or view with columns ' + 'SSO, First_Name, Last_Name, Team, Role, Picture. ' + 'See plugins/employees/README.md.'}, {'key': 'employee_db_name', 'label': 'Employee DB name', 'type': 'text', 'secret': False, 'default': 'wjf_employees'}, {'key': 'employee_db_user', 'label': 'Employee DB user', 'type': 'text', diff --git a/plugins/usb/README.md b/plugins/usb/README.md new file mode 100644 index 0000000..ae66985 --- /dev/null +++ b/plugins/usb/README.md @@ -0,0 +1,98 @@ +# USB plugin - CMMC USB check-in/out database contract + +The USB plugin tracks removable-media check-in/out for CMMC compliance. Device +and log state live in a **separate, read-write** MySQL database (`cmmc_usb`), +reached with parameterized pymysql via `cmmc_usb_connection()`. Names of people +are resolved from the HR employee directory (see the employees plugin). + +The plugin's own reference tables (`usbdevicetypes`, `usbdevices`, +`usbcheckouts`) live in the main app database; only the live check-in/out data +is in `cmmc_usb`. + +## Connection + +Credentials resolve **settings-first, then environment**, except the password +which is env-only (never stored in the app database). + +| Field | Setting key (editable in the setup wizard) | Env var (fallback) | +| -------- | ------------------------------------------ | ----------------------- | +| Host | `cmmc_usb_db_host` | `CMMC_USB_DB_HOST` | +| Database | `cmmc_usb_db_name` | `CMMC_USB_DB_NAME` | +| User | `cmmc_usb_db_user` | `CMMC_USB_DB_USER` | +| Password | (not stored) | `CMMC_USB_DB_PASSWORD` | + +Set the password in `.env`; the setup wizard's Features step shows the exact +line to paste. This database is **read-write** - the app account needs +`SELECT, INSERT, UPDATE`. Engine is MySQL/MariaDB (pymysql). + +## Required schema + +The plugin runs raw SQL against these tables (or **views** - see below). + +### `devices` +| Column | Type | Notes | +| ----------------- | ------- | -------------------------------------- | +| `device_id` | VARCHAR | Primary key; the device serial/tag | +| `device_desc` | VARCHAR | Description | +| `device_owner` | VARCHAR | Owner | +| `status` | VARCHAR | Check-in/out state | +| `locker_location` | VARCHAR | Where the device is stored | + +Operations: `SELECT` (list + by id), `INSERT` (register device), `UPDATE` +(edit fields, change status). + +### `checkinoutlog` +| Column | Type | Notes | +| ----------------- | --------- | ---------------------------------- | +| `log_id` | INT (PK) | Auto id | +| `badge_number` | VARCHAR | Person's badge | +| `device_id` | VARCHAR | FK to `devices.device_id` | +| `action` | VARCHAR | check-in / check-out | +| `timestamp` | DATETIME | When it happened | +| `scanned_viruses` | (text/int)| Scan result | +| `locker_location` | VARCHAR | Locker at time of event | +| `sanitized` | (bool/int)| Sanitization flag | + +Operations: `SELECT` (history per device), `INSERT` (log an event). + +### `users` +| Column | Type | Notes | +| -------------- | ------- | ------------------------------------- | +| `badge_number` | VARCHAR | Primary key; the scanned badge | +| `first_name` | VARCHAR | Given name | +| `last_name` | VARCHAR | Surname | + +Operations: `SELECT` by badge, `INSERT` (auto-add a badge on first scan). + +## Employee directory dependency + +To turn a scanned badge into a name, the plugin also reads the HR `employees` +directory (via the employees plugin's connection). A badge shaped `0BZ` +carries a PayNo (the digits); lookups try `employees.SSO` and `employees.PayNo`. +See `plugins/employees/README.md` for that schema and connection. + +## Adapting a different site schema (recommended: views) + +Sites whose USB-tracking database uses different table/column names should +create read-only/updatable **views** named `devices`, `checkinoutlog`, and +`users` that map local columns to the names above. Example: + +```sql +CREATE VIEW devices AS +SELECT + asset_tag AS device_id, + description AS device_desc, + owner AS device_owner, + state AS status, + storage_bay AS locker_location +FROM usb_assets; +``` + +Because the plugin writes to `devices`/`checkinoutlog`/`users`, either make the +views updatable (single-table views usually are) or expose real tables with +these column names. Grant the app account `SELECT, INSERT, UPDATE`. + +Notes: +- `device_id` and `badge_number` are the natural keys the plugin matches on. +- If `cmmc_usb` is unreachable, USB endpoints return an error and the rest of + the app keeps working (the feature degrades, it does not crash the app). diff --git a/plugins/usb/plugin.py b/plugins/usb/plugin.py index a1ed234..0cc99b2 100644 --- a/plugins/usb/plugin.py +++ b/plugins/usb/plugin.py @@ -55,7 +55,24 @@ class USBPlugin(BasePlugin): def get_models(self) -> List[Type]: """Return list of SQLAlchemy model classes.""" return [USBDeviceType, USBDevice, USBCheckout] - + + def get_config_schema(self) -> List[Dict]: + """CMMC USB check-in/out database connection. Host/name/user are settings + the wizard can edit; the password stays in .env (emitted, not stored).""" + return [ + {'key': 'cmmc_usb_db_host', 'label': 'USB DB host', 'type': 'text', + 'secret': False, 'default': 'localhost', + 'help': 'This DB must expose devices / checkinoutlog / users tables ' + '(or views). See plugins/usb/README.md.'}, + {'key': 'cmmc_usb_db_name', 'label': 'USB DB name', 'type': 'text', + 'secret': False, 'default': 'cmmc_usb'}, + {'key': 'cmmc_usb_db_user', 'label': 'USB DB user', 'type': 'text', + 'secret': False}, + {'key': 'cmmc_usb_db_password', 'label': 'USB DB password', 'type': 'password', + 'secret': True, 'envvar': 'CMMC_USB_DB_PASSWORD', + 'help': 'Stored in .env, not the database. The wizard shows the line to paste.'}, + ] + def init_app(self, app: Flask, db_instance) -> None: """Initialize plugin with Flask app.""" logger.info(f"USB plugin initialized (v{self.meta.version})") diff --git a/shopdb/utils/cmmc_usb_db.py b/shopdb/utils/cmmc_usb_db.py index 1611ba6..d2c2ccc 100644 --- a/shopdb/utils/cmmc_usb_db.py +++ b/shopdb/utils/cmmc_usb_db.py @@ -11,12 +11,28 @@ import pymysql from flask import current_app +def _setting_or_config(setting_key, config_key): + """Non-secret config: a saved Setting wins, else the env-backed app config. + + Lets the setup wizard edit host/name/user without touching .env, while the + password stays env-only. + """ + from shopdb.core.models import Setting + try: + row = Setting.query.filter_by(key=setting_key).first() + if row and row.value: + return row.value + except Exception: + pass + return current_app.config[config_key] + + def cmmc_usb_connection(): """Open a pymysql connection to the cmmc_usb DB.""" return pymysql.connect( - host=current_app.config['CMMC_USB_DB_HOST'], - user=current_app.config['CMMC_USB_DB_USER'], + host=_setting_or_config('cmmc_usb_db_host', 'CMMC_USB_DB_HOST'), + user=_setting_or_config('cmmc_usb_db_user', 'CMMC_USB_DB_USER'), password=current_app.config['CMMC_USB_DB_PASSWORD'], - database=current_app.config['CMMC_USB_DB_NAME'], + database=_setting_or_config('cmmc_usb_db_name', 'CMMC_USB_DB_NAME'), cursorclass=pymysql.cursors.DictCursor, )