Files
shopdb-flask/plugins/printedparts/plugin.py
cproudlock e4db112296 Use a valuetype the settings column actually documents
printedparts_kiosk_token declared valuetype 'secret'. The column documents
string, boolean, integer and json, and that was the only 'secret' in the
codebase - it worked only because Setting.get falls through to the raw string
when it recognises nothing.

Masking never depended on it: settings.py::_is_secret keys on the NAME
containing 'token', so the value is still hidden in API reads and in the audit
log. Changed to string, which is what it is.

Scope: product
2026-09-02 17:49:38 -04:00

216 lines
8.6 KiB
Python

"""Printedparts plugin main class.
3D-printed parts inventory + kiosk checkout. Quantity-based consumables:
one row is a KIND of part with a count, not an individually tracked asset,
so unlike most plugins this one seeds NO AssetType (ADR-001 assets are
one-row-per-physical-thing). See docs/proposals/printedparts-plugin.md.
"""
import json
import logging
from pathlib import Path
from typing import List, Optional, Type
from flask import Flask, Blueprint
from shopdb.plugins.base import BasePlugin, PluginMeta
from .models import PrintedItem, PrintedItemTransaction, PrintedItemFile
from .api import printedparts_bp
logger = logging.getLogger(__name__)
class PrintedpartsPlugin(BasePlugin):
"""3D-printed parts inventory + kiosk checkout."""
def __init__(self):
manifest_path = Path(__file__).parent / 'manifest.json'
with open(manifest_path) as f:
self._manifest = json.load(f)
@property
def meta(self) -> PluginMeta:
return PluginMeta(
name=self._manifest['name'],
version=self._manifest['version'],
description=self._manifest['description'],
author=self._manifest.get('author', ''),
dependencies=self._manifest.get('dependencies', []),
core_version=self._manifest.get('core_version', '>=0.1.0'),
api_prefix=self._manifest.get('api_prefix'),
)
def get_blueprint(self) -> Optional[Blueprint]:
return printedparts_bp
def get_models(self) -> List[Type]:
return [PrintedItem, PrintedItemTransaction, PrintedItemFile]
def init_app(self, app: Flask, db_instance) -> None:
logger.info(f'Printedparts plugin initialized (v{self.meta.version})')
def get_permissions(self) -> List:
"""RBAC permissions this plugin owns (seeded on install/enable)."""
return [
('printedparts.view', 'View 3D printed parts', 'printedparts'),
('printedparts.create', 'Create printed parts', 'printedparts'),
('printedparts.edit', 'Edit printed parts', 'printedparts'),
('printedparts.delete', 'Retire printed parts', 'printedparts'),
('printedparts.restock', 'Restock and adjust stock counts',
'printedparts'),
]
def get_settings_cards(self) -> List[dict]:
return [
{
'group': '3D Printed Parts',
'to': '/settings/printedparts',
'icon': 'box',
'title': '3D Parts Settings',
'description': 'Item code prefix, default threshold, kiosk '
'badge policy, low-stock alert recipients',
'position': 47,
},
]
def get_reports(self) -> List[dict]:
return [
{
'id': 'printedparts-stock',
'name': '3D Parts Stock',
'description': 'Stock levels with low-stock flags and the '
'cache-vs-ledger reconcile check',
'category': 'inventory',
'endpoint': '/api/printedparts/reports/stock',
},
{
'id': 'printedparts-consumption',
'name': '3D Parts Consumption',
'description': 'Takes per item over a date range',
'category': 'usage',
'endpoint': '/api/printedparts/reports/consumption',
},
{
'id': 'printedparts-by-person',
'name': '3D Parts by Person',
'description': 'Takes grouped by employee',
'category': 'usage',
'endpoint': '/api/printedparts/reports/by-person',
},
]
def get_navigation_items(self) -> List[dict]:
return [
{
'name': '3D Parts',
'icon': 'box',
'route': '/printedparts',
'position': 27,
# Named so the nav can hide it from anybody - a signed-out
# visitor included - who does not hold it.
'permission': 'printedparts.view',
'section': 'assets',
},
]
def on_install(self, app: Flask) -> None:
logger.info('Printedparts plugin installed')
def get_settings_defaults(self) -> List[dict]:
# The framework seeds these at install, at enable, and on every
# `flask plugin upgrade-all`, so a key added in a later version reaches
# a site that installed an earlier one.
return [
{
'key': 'printedparts_code_prefix',
'value': '3DP',
'valuetype': 'string',
'category': 'printedparts',
'description': 'Prefix for generated item codes',
},
{
'key': 'printedparts_label_prefix',
'value': '',
'valuetype': 'string',
'category': 'printedparts',
# Public: the parts kiosk runs logged out, so the anonymous
# read has to be allowed or the prefix never renders there.
'public': True,
'description': 'Leading text on the physical gage-lab labels, '
'shown at the kiosk before the number box so '
'operators type only the digits. Empty shows no '
'prefix. Site-specific: set it to whatever this '
"site's labels are printed with, e.g. WJ",
},
{
'key': 'printedparts_default_threshold',
'value': '5',
'valuetype': 'integer',
'category': 'printedparts',
'description': 'Default low-stock threshold for new items',
},
{
'key': 'printedparts_kiosk_enabled',
'value': 'false',
'valuetype': 'boolean',
'category': 'printedparts',
'description': 'Serve the parts kiosk routes. Off by default: '
'they are the only unauthenticated write in the '
'product, so a site opts in',
},
{
'key': 'printedparts_kiosk_token',
'value': '',
# string, not 'secret': the column documents string/boolean/
# integer/json only. Masking is keyed on the NAME containing
# 'token' (settings.py::_is_secret), so this is still hidden in
# the API and the audit log.
'valuetype': 'string',
'category': 'printedparts',
'description': 'Device token the kiosk sends as X-Kiosk-Token. '
'Generate one, then open the kiosk once as '
'/parts-kiosk?token=<value> on each screen',
},
{
'key': 'printedparts_unknown_badge',
'value': 'deny',
'valuetype': 'string',
'category': 'printedparts',
'description': 'Kiosk policy when a badge resolves to no '
'employee: allow or deny',
},
{
'key': 'printedparts_alert_email',
'value': '',
'valuetype': 'string',
'category': 'printedparts',
'description': 'Comma-separated low-stock alert recipients; '
'empty uses the site alert_recipients',
},
{
'key': 'printedparts_alert_userids',
'value': '',
'valuetype': 'string',
'category': 'printedparts',
'description': 'Comma-separated shopdb user ids whose account '
'emails receive low-stock alerts',
},
{
'key': 'printedparts_alert_roleids',
'value': '',
'valuetype': 'string',
'category': 'printedparts',
'description': 'Comma-separated role ids; every active member '
'of these roles receives low-stock alerts',
},
{
'key': 'printedparts_alert_supportteamid',
'value': '',
'valuetype': 'string',
'category': 'printedparts',
'description': 'Support team whose webhook receives low-stock '
'alerts; empty uses the site alert_webhook_url',
},
]