mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-29 16:17:25 +09:00
328 more routes used a JSON body as a dict straight from
request.get_json(), so a body of null, [] or a bare string crashed them
into a 500. Bare reads become json_object(); the
'get_json(silent=True) or {}' reads of action routes become
json_object(optional=True), which still treats a missing body as {}.
Routes that take arrays or pass the body through untouched are left
alone. The full backend suite shows no new failures.
Also carries three files' bare section citations (apps, backups,
notifications) and the extended citation guard, which needed them.
99 lines
4.4 KiB
Python
99 lines
4.4 KiB
Python
"""Marketplace API — the extension registry surface.
|
|
|
|
The legacy DB-seeded ``Extension``/``ExtensionInstall`` catalog was retired
|
|
(#51): nothing ever populated it on a real panel, so it fed Browse an empty
|
|
third lane that was redundant with the three real sources — the builtin
|
|
folder scan (``/plugins/builtin``), the remote registry (below), and live
|
|
``InstalledPlugin`` state. This blueprint now serves only the registry.
|
|
"""
|
|
from flask import Blueprint, jsonify, request
|
|
from flask_jwt_extended import jwt_required
|
|
from app.services.audit_service import AuditService
|
|
from app.models.audit_log import AuditLog
|
|
from app.middleware.rbac import get_current_user
|
|
from app.error_reporting import unexpected_response
|
|
from app.api._request import json_object
|
|
|
|
marketplace_bp = Blueprint('marketplace', __name__)
|
|
|
|
|
|
@marketplace_bp.route('/registry', methods=['GET'])
|
|
@jwt_required()
|
|
def list_registry():
|
|
"""Return the remote-registry extensions (with live install state), for the
|
|
Browse merge. Read-only; offline-tolerant (falls back to a bundled index).
|
|
|
|
Bundled entries (builtins listed for the public catalog) are excluded by
|
|
default to avoid duplicating the builtin cards; pass
|
|
``?include_bundled=true`` for the complete catalog."""
|
|
from app.services import registry_service
|
|
include_bundled = request.args.get('include_bundled', '').lower() in ('1', 'true', 'yes')
|
|
return jsonify({
|
|
'extensions': registry_service.list_catalog(include_bundled=include_bundled),
|
|
'source': registry_service.registry_source_label(),
|
|
})
|
|
|
|
|
|
@marketplace_bp.route('/registry/<slug>/install', methods=['POST'])
|
|
@jwt_required()
|
|
def install_registry(slug):
|
|
"""Install a registry extension by slug. Checksum-verified (the entry's
|
|
sha256, when present, must match before extraction)."""
|
|
user = get_current_user()
|
|
if not user or not user.is_admin:
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
|
|
# Trust gate: an unreviewed community extension, or any entry with no
|
|
# pinned checksum (possible even for first_party), installs only after an
|
|
# explicit risk acknowledgment — the Marketplace shows a confirmation
|
|
# dialog and resends with acknowledge_risk: true. first_party / reviewed
|
|
# entries install as-is. `reason` tells the UI which case it is so the
|
|
# dialog copy stays accurate.
|
|
from app.services import registry_service
|
|
entry = registry_service.get_entry(slug)
|
|
# Hidden means not installable: unreviewed entries only exist in
|
|
# development contexts (see registry_service._show_unreviewed).
|
|
if entry is not None and entry.get('trust') == 'unreviewed' \
|
|
and not registry_service._show_unreviewed():
|
|
return jsonify({'error': 'Extension not found in the registry'}), 404
|
|
body = json_object(optional=True)
|
|
trust = (entry or {}).get('trust', 'unreviewed')
|
|
acknowledged = body.get('acknowledge_risk') is True
|
|
# Consent gate (shared with the update path — audits M2/M3):
|
|
# unreviewed (dev-stage) > unverified (no checksum) > signature gate
|
|
# (untrusted key, or unsigned first-party = possible downgrade).
|
|
reason, message = registry_service.consent_gate(entry, acknowledged)
|
|
if reason:
|
|
return jsonify({
|
|
'error': f'{message} Resend with acknowledge_risk: true to proceed.',
|
|
'trust': trust,
|
|
'reason': reason,
|
|
'requires_acknowledgment': True,
|
|
}), 409
|
|
|
|
from app.services.plugin_service import install_registry_extension
|
|
try:
|
|
plugin = install_registry_extension(slug, user_id=user.id)
|
|
details = {'name': plugin.name, 'version': plugin.version, 'source': 'registry'}
|
|
if acknowledged:
|
|
details['trust'] = trust
|
|
details['acknowledged_risk'] = True
|
|
AuditService.log(
|
|
action=AuditLog.ACTION_RESOURCE_CREATE,
|
|
user_id=user.id,
|
|
target_type='plugin',
|
|
target_id=plugin.id,
|
|
details=details,
|
|
)
|
|
# Opt-in anonymous install ping (default OFF; #17). Best-effort — never
|
|
# affects the install result.
|
|
try:
|
|
registry_service.record_install(plugin.slug, plugin.version)
|
|
except Exception:
|
|
pass
|
|
return jsonify(plugin.to_dict()), 201
|
|
except ValueError as e:
|
|
return jsonify({'error': str(e)}), 400
|
|
except Exception as exc: # noqa: BLE001 - reported, not swallowed
|
|
return unexpected_response(exc)
|