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
+7 -1
View File
@@ -22,7 +22,13 @@
<div class="topbar-user">
<span class="user-badge">{{ current_user.username[0].upper() }}</span>
<span class="user-name">{{ current_user.username }}</span>
{% if current_user.is_admin %}<span class="role-tag">admin</span>{% endif %}
{% if current_user.role == 'admin' %}
<span class="role-tag role-tag--admin">admin</span>
{% elif current_user.role == 'advanced' %}
<span class="role-tag role-tag--advanced">advanced</span>
{% else %}
<span class="role-tag role-tag--standard">standard</span>
{% endif %}
<a href="{{ url_for('auth.logout') }}" class="btn-logout" title="Sign out"></a>
</div>
</header>
+11 -5
View File
@@ -41,10 +41,12 @@
<td><strong>{{ user.username }}</strong></td>
<td class="text-muted">{{ user.email }}</td>
<td>
{% if user.is_admin %}
{% if user.role == 'admin' %}
<span class="badge badge-admin">admin</span>
{% elif user.role == 'advanced' %}
<span class="badge badge-advanced">advanced</span>
{% else %}
<span class="badge badge-user">user</span>
<span class="badge badge-user">standard</span>
{% endif %}
</td>
<td class="text-muted">
@@ -80,9 +82,13 @@
<option value="none" {% if not (cur_access and cur_access.is_active) %}selected{% endif %}>
— no access
</option>
<option value="user"
{% if cur_access and cur_access.is_active and (not cur_access.app_role or cur_access.app_role == 'user') %}selected{% endif %}>
✓ user
<option value="standard"
{% if cur_access and cur_access.is_active and cur_access.app_role == 'standard' %}selected{% endif %}>
👁 standard
</option>
<option value="advanced"
{% if cur_access and cur_access.is_active and cur_access.app_role == 'advanced' %}selected{% endif %}>
✏ advanced
</option>
<option value="admin"
{% if cur_access and cur_access.is_active and cur_access.app_role == 'admin' %}selected{% endif %}>
+9 -8
View File
@@ -34,13 +34,13 @@
<label class="form-label">Password</label>
<input type="password" name="password" class="form-input" required />
</div>
<div class="form-group form-group--check">
<div class="form-group">
<label class="form-label">Portal Role</label>
<label class="toggle">
<input type="checkbox" name="is_admin" />
<span class="toggle-slider"></span>
<span class="toggle-label">Platform administrator</span>
</label>
<select name="portal_role" class="form-select">
<option value="standard">Standard User — read-only access in apps</option>
<option value="advanced">Advanced User — create &amp; edit content in apps</option>
<option value="admin">Administrator — full access + user management</option>
</select>
</div>
</div>
@@ -61,8 +61,9 @@
<select name="role_{{ app['id'] }}" class="form-select app-role-new-select"
data-color="{{ app.color }}">
<option value="none">— No access</option>
<option value="user">✓ User</option>
<option value="admin"> Admin</option>
<option value="standard">👁 Standard — read-only</option>
<option value="advanced"> Advanced — create &amp; edit</option>
<option value="admin">★ Admin — full access</option>
</select>
</div>
{% endfor %}