Files
enterprise_digital-platform/portal/app/templates/settings/index.html
T
ske087 1f6217d347 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
2026-07-07 00:08:46 +03:00

131 lines
5.4 KiB
HTML

{% extends 'base.html' %}
{% block title %}Settings — Enterprise Digital Platform{% endblock %}
{% block content %}
<div class="settings-wrapper">
<div class="settings-header">
<h2 class="section-title">Settings</h2>
<div class="settings-tabs">
<a href="{{ url_for('settings.index') }}" class="tab-link active">Users &amp; Access</a>
<a href="{{ url_for('settings.api_keys') }}" class="tab-link">API Keys</a>
<a href="{{ url_for('settings.modules') }}" class="tab-link">Modules</a>
</div>
</div>
<div class="settings-section">
<div class="section-toolbar">
<h3>Portal Users</h3>
<a href="{{ url_for('settings.new_user') }}" class="btn btn-primary btn-sm">+ New User</a>
</div>
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Last Login</th>
{% for app in registered_apps %}
<th class="app-col" style="border-top: 3px solid {{ app.color }};">
{{ app.icon }} {{ app.name }}<br/>
<small style="font-weight:400; color:var(--text-muted);">access / role</small>
</th>
{% endfor %}
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td><strong>{{ user.username }}</strong></td>
<td class="text-muted">{{ user.email }}</td>
<td>
{% 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">standard</span>
{% endif %}
</td>
<td class="text-muted">
{{ user.last_login.strftime('%Y-%m-%d %H:%M') if user.last_login else '—' }}
</td>
{# Per-app role dropdown — auto-submits on change #}
{% for app in registered_apps %}
{% set cur_access = user.app_accesses.filter_by(app_name=app['id']).first() %}
{% set cur_role = cur_access.app_role if cur_access and cur_access.is_active and cur_access.app_role else ('active' if (cur_access and cur_access.is_active) else 'none') %}
<td class="center">
<form method="POST" action="{{ url_for('settings.update_access', user_id=user.id) }}"
class="inline-form">
{# Carry all other apps' current values as hidden inputs #}
{% for other in registered_apps %}
{% if other['id'] != app['id'] %}
{% set oa = user.app_accesses.filter_by(app_name=other['id']).first() %}
{% if oa and oa.is_active %}
<input type="hidden" name="role_{{ other['id'] }}"
value="{{ oa.app_role if oa.app_role else 'user' }}" />
{% else %}
<input type="hidden" name="role_{{ other['id'] }}" value="none" />
{% endif %}
{% endif %}
{% endfor %}
<select name="role_{{ app['id'] }}"
class="app-role-select"
style="--app-color: {{ app.color }};"
onchange="this.closest('form').submit()"
title="{{ app['name'] }} access for {{ user.username }}"
{% if user.id == current_user.id and app['id'] == 'portal' %}disabled{% endif %}>
<option value="none" {% if not (cur_access and cur_access.is_active) %}selected{% endif %}>
— no access
</option>
<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 %}>
★ admin
</option>
</select>
</form>
</td>
{% endfor %}
<td>
{% if user.id != current_user.id %}
<form method="POST" action="{{ url_for('settings.delete_user', user_id=user.id) }}"
onsubmit="return confirm('Delete user {{ user.username }}?')">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script>
document.querySelectorAll('.app-role-select').forEach(function(sel) {
function refresh() {
if (sel.value !== 'none') sel.classList.add('has-access');
else sel.classList.remove('has-access');
}
refresh();
sel.addEventListener('change', refresh);
});
</script>
{% endblock %}