mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 13:35:12 +09:00
Completes the deferred half of #30. InstalledPlugin gains config_json (migration 045); admin-gated GET/PUT /api/v1/plugins/<id>/config persists values; plugins read them via the new plugins_sdk.config(slug) accessor. The Marketplace Installed tab shows a Configure button for plugins declaring a config_schema and renders a real form (string/number/integer/boolean/enum/secret fields, defaults, hints). Values may hold secrets so they are excluded from to_dict() and only served by the admin endpoint. Documented in docs/EXTENSIONS.md. 2 new tests (config round-trip + SDK accessor + secret non-leak; non-object rejection); pipeline suite 18 green; lint 0 errors; build clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
143 lines
3.9 KiB
Python
143 lines
3.9 KiB
Python
"""
|
|
ServerKit Backend Plugin SDK.
|
|
|
|
Plugins should depend on names re-exported here rather than reaching into
|
|
host internals directly. As long as this surface stays stable, host-side
|
|
refactors don't break installed plugins.
|
|
|
|
Typical plugin module:
|
|
|
|
from flask import Blueprint, request, jsonify
|
|
from app.plugins_sdk import (
|
|
db, jwt_required, current_user, audit, logger,
|
|
)
|
|
|
|
my_bp = Blueprint('my_plugin', __name__)
|
|
log = logger(__name__)
|
|
|
|
@my_bp.route('/things', methods=['GET'])
|
|
@jwt_required()
|
|
def list_things():
|
|
user = current_user()
|
|
# ... do work, use db.session, etc ...
|
|
return jsonify({'ok': True})
|
|
|
|
The lifecycle hook contract (called by plugin_service when installing /
|
|
uninstalling): a single positional arg — the InstalledPlugin row.
|
|
|
|
# In plugins/<slug>/lifecycle.py
|
|
from app.plugins_sdk import db, logger
|
|
log = logger(__name__)
|
|
|
|
def on_install(plugin):
|
|
log.info(f'Setting up {plugin.slug}')
|
|
|
|
def on_uninstall(plugin):
|
|
log.info(f'Tearing down {plugin.slug}')
|
|
"""
|
|
import logging
|
|
|
|
from app import db, jwt
|
|
from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt
|
|
|
|
|
|
def current_user():
|
|
"""Return the current authenticated user, or None if no JWT context.
|
|
|
|
Resolves the JWT identity (a stringified user id, per the host's
|
|
@jwt.user_identity_loader) into a User row.
|
|
"""
|
|
try:
|
|
uid = get_jwt_identity()
|
|
except Exception:
|
|
return None
|
|
if uid is None:
|
|
return None
|
|
from app.models.user import User
|
|
try:
|
|
return User.query.get(int(uid))
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def logger(name):
|
|
"""Return a logger scoped to a plugin module name."""
|
|
return logging.getLogger(name)
|
|
|
|
|
|
def audit(action, target_type, target_id=None, details=None, user_id=None):
|
|
"""Write an audit log entry from a plugin.
|
|
|
|
Thin wrapper around AuditService so plugins don't have to know its
|
|
import path. `user_id` defaults to the current authenticated user.
|
|
"""
|
|
if user_id is None:
|
|
u = current_user()
|
|
user_id = u.id if u else None
|
|
from app.services.audit_service import AuditService
|
|
return AuditService.log(
|
|
action=action,
|
|
user_id=user_id,
|
|
target_type=target_type,
|
|
target_id=target_id,
|
|
details=details or {},
|
|
)
|
|
|
|
|
|
# AI extension SDK — plugins do `from app.plugins_sdk import ai` then register
|
|
# tools/context against the core assistant (see plugins_sdk/ai.py). Imported
|
|
# lazily-safe: ai.py only depends on the AI tool registry, not on a running app.
|
|
from app.plugins_sdk import ai
|
|
from app.plugins_sdk import permissions
|
|
from app.plugins_sdk import sockets
|
|
from app.queue_bus.sdk import QueueBusSdk
|
|
from app.notifications.sdk import NotifySdk
|
|
from app.jobs.sdk import JobsSdk
|
|
|
|
queue = QueueBusSdk()
|
|
notify = NotifySdk()
|
|
jobs = JobsSdk()
|
|
|
|
|
|
def panel_version():
|
|
"""The panel's version string (for compat checks inside a plugin)."""
|
|
from app.utils.version import get_panel_version
|
|
return get_panel_version()
|
|
|
|
|
|
def config(slug):
|
|
"""Saved config values for an installed plugin (empty dict if none).
|
|
|
|
The manifest's ``config_schema`` documents the fields; admins edit the
|
|
values from Marketplace → Installed → Configure. Read-only here — the
|
|
panel owns writes (PUT /api/v1/plugins/<id>/config).
|
|
"""
|
|
from app.models.plugin import InstalledPlugin
|
|
p = InstalledPlugin.query.filter_by(slug=slug).first()
|
|
return dict(p.config or {}) if p else {}
|
|
|
|
|
|
# Capability gate: `require(slug, 'docker')` raises unless the plugin declared
|
|
# that permission in its manifest. See app/plugins_sdk/permissions.py.
|
|
require_permission = permissions.require
|
|
|
|
__all__ = [
|
|
'db',
|
|
'jwt',
|
|
'jwt_required',
|
|
'get_jwt_identity',
|
|
'get_jwt',
|
|
'current_user',
|
|
'logger',
|
|
'audit',
|
|
'ai',
|
|
'queue',
|
|
'notify',
|
|
'jobs',
|
|
'permissions',
|
|
'require_permission',
|
|
'panel_version',
|
|
'config',
|
|
'sockets',
|
|
]
|