feat: 3-tier role system across all platform apps

Portal:
- Replace is_admin boolean with role column (admin/advanced/standard)
- Settings UI: 3-tier portal role select + per-app role dropdowns
- /portal-return endpoint: re-establishes session from JWT for sub-app back-links
- /api/internal/nv-users: internal endpoint for NetworkView user sync
- portal/migrate_roles.py: one-time DB migration script
- Role badges (admin/advanced/standard) in topbar and settings table

DigiServer:
- Add editor and viewer roles (portal advanced->editor, standard->viewer)
- PlaylistPermission model: grant viewer users edit access to specific playlists
- app/utils/access.py: shared editor_required, admin_required, can_edit_playlist helpers
- Content routes: editor_required on create/delete, per-playlist permission check on mutations
- Admin: playlist_permissions route + template to manage viewer playlist grants
- Base template: hide Admin nav for viewers, Portal button (⬡) returns to portal
- content_list_new: hide create/delete for viewers; Manage vs View button per permission
- manage_playlist_content: view-only mode when user lacks edit permission

NetworkView:
- backend/src/middleware/rbac.js: requireRole + requireWriteAccess helpers
- site_permissions table: one site per advanced user
- All mutating routes guarded (admin=all, advanced=assigned site, viewer=read-only)
- Portal SSO auto-upsert: user row synced from X-Auth-Role on every request
- GET /api/users: merges portal users list with local NV data (all 4 portal users visible)
- GET/PUT /api/users/:id/site-permission: assign site to advanced user
- Settings Users tab: role badges, site dropdown for advanced, (portal only) indicator
- Sidebar: ⬡ Portal button between Settings and Logout
- Frontend build: VITE_API_BASE=/networkview/api now set in start-dev.sh

IT Assets / Server Monitor:
- portal_sso.py updated: map advanced->editor/viewer, standard->readonly
- AdminUser model: add editor role + is_editor property
This commit is contained in:
ske087
2026-07-07 00:08:46 +03:00
parent 5762dd420d
commit 1f6217d347
41 changed files with 1028 additions and 153 deletions
+55 -14
View File
@@ -9,26 +9,15 @@ from typing import Optional
from app.extensions import db, bcrypt
from app.models import User, Player, Content, ServerLog, Playlist, HTTPSConfig
from app.models.playlist_permission import PlaylistPermission
from app.utils.logger import log_action
from app.utils.caddy_manager import CaddyConfigGenerator
from app.utils.nginx_config_reader import get_nginx_status
from app.utils.access import admin_required, editor_required
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
def admin_required(f):
"""Decorator to require admin role for route access."""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated:
flash('Please login to access this page.', 'warning')
return redirect(url_for('auth.login'))
if current_user.role != 'admin':
log_action('warning', f'Unauthorized admin access attempt by {current_user.username}')
flash('You do not have permission to access this page.', 'danger')
return redirect(url_for('main.dashboard'))
return f(*args, **kwargs)
return decorated_function
# admin_required and editor_required imported from app.utils.access
@admin_bp.route('/')
@@ -203,6 +192,58 @@ def user_management():
return redirect(url_for('admin.admin_panel'))
# ── Playlist permissions (viewer-role users) ───────────────────────────────────
@admin_bp.route('/user/<int:user_id>/playlist-permissions')
@login_required
@admin_required
def playlist_permissions(user_id: int):
"""Show/manage which playlists a viewer-role user may edit."""
user = User.query.get_or_404(user_id)
playlists = Playlist.query.order_by(Playlist.name).all()
granted_ids = {
p.playlist_id
for p in PlaylistPermission.query.filter_by(user_id=user_id).all()
}
return render_template(
'admin/playlist_permissions.html',
target_user=user,
playlists=playlists,
granted_ids=granted_ids,
)
@admin_bp.route('/user/<int:user_id>/playlist-permissions/save', methods=['POST'])
@login_required
@admin_required
def save_playlist_permissions(user_id: int):
"""Save (overwrite) the playlist edit permissions for a viewer user."""
user = User.query.get_or_404(user_id)
all_playlists = Playlist.query.all()
# The form sends one checkbox per playlist: name="playlist_<id>" value="1"
new_ids = {
pl.id for pl in all_playlists
if request.form.get(f'playlist_{pl.id}') == '1'
}
# Current grants
existing = {p.playlist_id: p for p in PlaylistPermission.query.filter_by(user_id=user_id).all()}
# Add new grants
for pid in new_ids - set(existing.keys()):
db.session.add(PlaylistPermission(user_id=user_id, playlist_id=pid))
# Remove revoked grants
for pid in set(existing.keys()) - new_ids:
db.session.delete(existing[pid])
db.session.commit()
log_action('info', f'Playlist permissions updated for user "{user.username}" by {current_user.username}')
flash(f'Playlist permissions updated for "{user.username}".', 'success')
return redirect(url_for('admin.playlist_permissions', user_id=user_id))
@admin_bp.route('/user/<int:user_id>/password', methods=['POST'])
@login_required
@admin_required