"""Tech Tools plugin. A section for small technician utilities. MOST tools here run entirely in the browser - no API, no tables, no state - which is deliberate: these are the things someone reaches for at a bench with no network guarantee, and an air-gapped site gets them for free. SECURE SEND IS THE EXCEPTION, and it is why this plugin now has a blueprint, a table and a migration chain where it once had none. A link that expires and dies on first read cannot be browser-only: something has to hold the ciphertext until it is claimed, and forget it afterwards. The secret itself is still encrypted in the browser - the key travels in the URL fragment and never reaches this server - so what is stored is ciphertext nobody on this side can read. The plugin also exists so the section can carry a nav entry and be included in or left out of a per-site build (ADR-013). """ import logging from typing import List, Optional, Type from flask import Blueprint, Flask from shopdb.plugins.base import BasePlugin, PluginMeta from .models import SecureSecret from .api import tools_bp logger = logging.getLogger(__name__) class ToolsPlugin(BasePlugin): """Registers the Tech Tools section and the one tool that needs a server.""" @property def meta(self) -> PluginMeta: return PluginMeta( name='tools', version='1.0.0', description=('Tech Tools: technician utilities, mostly browser-side ' '(barcode/QR labels) plus secure send for handing ' 'somebody a credential by one-view link'), author='ShopDB Team', dependencies=[], core_version='>=0.16.0,<1.0.0', api_prefix='/api/tools', ) def get_blueprint(self) -> Optional[Blueprint]: # Secure send only. Every other tool is still browser-side. return tools_bp def get_models(self) -> List[Type]: return [SecureSecret] def get_permissions(self) -> List: """Who may CREATE a secret, when guests are not allowed. The default access model: signed in, and holding this. Reading needs no permission at all - the recipient usually has no account here and the unguessable token is the authorisation. A site that turns on securesend_allow_guests bypasses this for people with no account; it still applies to anyone who IS signed in, so a member without it cannot create one while a guest can. That is deliberate: opening the door to guests is a decision about the door, not about revoking what a role does or does not hold. """ return [ ('tools.secrets', 'Create secure send links', 'tools'), ] def get_settings_defaults(self) -> List[dict]: """The one switch this plugin owns. Public so the create page can read it BEFORE login and show a guest the form rather than a sign-in wall. It says nothing sensitive: whether the door is open is visible to anyone who tries the door. """ return [ { 'key': 'securesend_allow_guests', 'value': 'false', 'valuetype': 'boolean', 'category': 'tools', 'public': True, 'description': ('Let people with no account create secure send ' 'links. Off by default: a guest secret has no ' 'creator, so the audit trail can say when and ' 'from where but never who. Receiving a secret ' 'never needs an account either way.'), }, ] def get_settings_cards(self) -> List[dict]: """A page for the one switch this plugin owns (ADR-010). get_settings_defaults SEEDS the row; this is what gives somebody a place to change it. Declaring only the first left the setting in the database with no way to reach it, which is indistinguishable from the feature not existing. """ return [ { 'group': 'Tools', 'to': '/settings/secure-send', # From the settings iconMap; an unknown key falls back to a # puzzle piece, which reads as "nobody chose this". 'icon': 'wrench', 'title': 'Secure Send', 'description': 'Whether people without an account can create ' 'one-view password links', 'position': 60, }, ] def get_navigation_items(self) -> List[dict]: return [ { 'name': 'Tech Tools', 'icon': 'wrench', 'route': '/tools', 'position': 47, }, ] def on_install(self, app: Flask) -> None: logger.info('Tools plugin installed')