slides: gate management on slides.manage permission (grantable to non-admin curator)
Some checks failed
CI / backend (push) Failing after 1m54s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 10s
CI / migrations-mysql (push) Failing after 8s

The lobby-display and screensaver slide manager was admin-only. Add a shared
slides.manage permission so a curator can manage both surfaces without full
admin. Admins keep access via the require_permission admin bypass.

Backend:
- plugins/slides/api/routes.py: all 5 management routes require slides.manage
- plugins/slides/plugin.py: declare it via get_permissions(); nav item carries
  the permission so the frontend can gate visibility
- shopdb/core/api/auth.py: login response now returns the user's permissions
  (matches /me) so the frontend authStore has them on fresh login

Frontend:
- stores/auth.js: hasPermission(name) getter (admin true, else granted list)
- router/index.js: guard supports requiresPermission
- views/AppLayout.vue: hide nav items whose permission the user lacks
- plugins/slides/frontend/routes.js: slide manager gated requiresPermission

Tests: no-perm user 403, curator role with the perm 200 (+ login advertises
it), admin 200 via bypass.

Deploy: run `flask seed permissions` to create the row, then grant it to a
role in Settings > Users & Roles.
This commit is contained in:
cproudlock
2026-07-29 08:41:11 -04:00
parent 7a7c7f37d5
commit 174c6c0b9a
8 changed files with 106 additions and 10 deletions

View File

@@ -2,7 +2,8 @@
Image files live on disk at instance/slides/<surface>/; TvSlide rows hold play
order + per-slide seconds. Feed + image routes are PUBLIC (kiosks/screensaver
have no auth); management routes are admin-only.
have no auth); management routes require the slides.manage permission (admins
have it by default).
"""
import os
@@ -12,7 +13,8 @@ from flask import Blueprint, request, current_app, jsonify, send_from_directory
from flask_jwt_extended import jwt_required
from werkzeug.utils import secure_filename
from shopdb.api import db, success_response, error_response, ErrorCodes, require_role
from shopdb.api import (db, success_response, error_response, ErrorCodes,
require_permission)
from ..models import TvSlide
@@ -88,7 +90,7 @@ def serve_image(surface, filename):
@slides_bp.route('/<surface>', methods=['GET'])
@jwt_required()
@require_role('admin')
@require_permission('slides.manage')
def list_slides(surface):
if not _valid_surface(surface):
return error_response(ErrorCodes.VALIDATION_ERROR, 'Unknown surface')
@@ -106,7 +108,7 @@ def list_slides(surface):
@slides_bp.route('/<surface>/upload', methods=['POST'])
@jwt_required()
@require_role('admin')
@require_permission('slides.manage')
def upload_slides(surface):
if not _valid_surface(surface):
return error_response(ErrorCodes.VALIDATION_ERROR, 'Unknown surface')
@@ -151,7 +153,7 @@ def upload_slides(surface):
@slides_bp.route('/<surface>/order', methods=['POST'])
@jwt_required()
@require_role('admin')
@require_permission('slides.manage')
def reorder_slides(surface):
if not _valid_surface(surface):
return error_response(ErrorCodes.VALIDATION_ERROR, 'Unknown surface')
@@ -167,7 +169,7 @@ def reorder_slides(surface):
@slides_bp.route('/<surface>/delete', methods=['POST'])
@jwt_required()
@require_role('admin')
@require_permission('slides.manage')
def delete_slides(surface):
if not _valid_surface(surface):
return error_response(ErrorCodes.VALIDATION_ERROR, 'Unknown surface')
@@ -193,7 +195,7 @@ def delete_slides(surface):
@slides_bp.route('/<surface>/<int:slideid>', methods=['PATCH'])
@jwt_required()
@require_role('admin')
@require_permission('slides.manage')
def update_slide(surface, slideid):
if not _valid_surface(surface):
return error_response(ErrorCodes.VALIDATION_ERROR, 'Unknown surface')