Setup wizard P2: per-plugin config schema + settings-first creds
- Plugin contract gains get_config_schema(); the plugins list API returns it. Employees plugin declares its directory-DB fields (host/name/user + password). - employee_connection reads host/name/user settings-first (env fallback); the password stays env-only. - Setup wizard Features step renders each enabled plugin's config: non-secret fields save to settings; secrets are never stored - the wizard emits .env lines to paste. Fixed the plugins-list data path (data.plugins). - Settings PUT now upserts (creates the row on first write) so plugin-config keys can be saved without pre-seeding. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -112,8 +112,11 @@ def update_setting(key: str):
|
||||
|
||||
setting = Setting.query.filter_by(key=key).first()
|
||||
|
||||
# Upsert: create the row on first write (e.g. plugin config keys the setup
|
||||
# wizard saves). New keys default to a plugin-scoped string setting.
|
||||
if not setting:
|
||||
return error_response(ErrorCodes.NOT_FOUND, f'Setting {key} not found', http_code=404)
|
||||
setting = Setting(key=key, value='', valuetype='string', category='plugin')
|
||||
db.session.add(setting)
|
||||
|
||||
# Track old value for audit
|
||||
old_value = setting.value
|
||||
|
||||
@@ -143,6 +143,10 @@ class PluginManager:
|
||||
meta = temp.meta
|
||||
state = self.registry.get(name)
|
||||
|
||||
try:
|
||||
config_schema = temp.get_config_schema()
|
||||
except Exception:
|
||||
config_schema = []
|
||||
available.append({
|
||||
'name': meta.name,
|
||||
'version': meta.version,
|
||||
@@ -151,7 +155,8 @@ class PluginManager:
|
||||
'dependencies': meta.dependencies,
|
||||
'installed': state is not None,
|
||||
'enabled': state.enabled if state else False,
|
||||
'installedat': state.installed_at if state else None
|
||||
'installedat': state.installed_at if state else None,
|
||||
'config_schema': config_schema,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f"Error inspecting plugin {name}: {e}")
|
||||
|
||||
@@ -74,6 +74,22 @@ class BasePlugin(ABC):
|
||||
"""Return dict of service name -> service class."""
|
||||
return {}
|
||||
|
||||
def get_config_schema(self) -> List[Dict]:
|
||||
"""Declare the config fields this plugin needs, for the setup wizard.
|
||||
|
||||
Each field is a dict:
|
||||
key - the Setting key (non-secret) it maps to
|
||||
label - human label
|
||||
type - 'text' | 'number' | 'password'
|
||||
secret - True for credentials; these are NOT stored in the DB, the
|
||||
wizard emits an .env line for the operator to paste instead
|
||||
envvar - (secret only) the .env variable name to emit
|
||||
default - optional default shown as a placeholder
|
||||
help - optional hint
|
||||
Return [] (default) if the plugin needs no configuration.
|
||||
"""
|
||||
return []
|
||||
|
||||
def get_setting(self, key: str, default=None):
|
||||
"""Read a plugin-scoped setting from the core Setting store.
|
||||
|
||||
|
||||
@@ -9,12 +9,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 employee_connection():
|
||||
"""Open a pymysql connection to the employee directory DB."""
|
||||
return pymysql.connect(
|
||||
host=current_app.config['EMPLOYEE_DB_HOST'],
|
||||
user=current_app.config['EMPLOYEE_DB_USER'],
|
||||
host=_setting_or_config('employee_db_host', 'EMPLOYEE_DB_HOST'),
|
||||
user=_setting_or_config('employee_db_user', 'EMPLOYEE_DB_USER'),
|
||||
password=current_app.config['EMPLOYEE_DB_PASSWORD'],
|
||||
database=current_app.config['EMPLOYEE_DB_NAME'],
|
||||
database=_setting_or_config('employee_db_name', 'EMPLOYEE_DB_NAME'),
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user