1f6217d347
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
98 lines
3.7 KiB
HTML
98 lines
3.7 KiB
HTML
{% extends 'base.html' %}
|
|
{% block title %}New User — Settings{% 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 & 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>Create New User</h3>
|
|
<a href="{{ url_for('settings.index') }}" class="btn btn-sm btn-secondary">← Back</a>
|
|
</div>
|
|
|
|
<form method="POST" class="form-card">
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Username</label>
|
|
<input type="text" name="username" class="form-input" required autofocus />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Email</label>
|
|
<input type="email" name="email" class="form-input" required />
|
|
</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Password</label>
|
|
<input type="password" name="password" class="form-input" required />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Portal Role</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 & edit content in apps</option>
|
|
<option value="admin">Administrator — full access + user management</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Application Access & Role</label>
|
|
<p style="font-size:0.82rem; color:var(--text-secondary); margin-bottom:0.75rem;">
|
|
Select the access level for each application. "Admin" grants full management rights
|
|
inside that app regardless of the portal role above.
|
|
</p>
|
|
<div class="app-role-grid">
|
|
{% for app in registered_apps %}
|
|
<div class="app-role-card" style="--app-color: {{ app.color }}; border-top-color: {{ app.color }};">
|
|
<div class="app-role-card-header">
|
|
<span style="font-size:1.5rem;">{{ app.icon }}</span>
|
|
<strong>{{ app.name }}</strong>
|
|
</div>
|
|
<p class="app-role-desc">{{ app.description }}</p>
|
|
<select name="role_{{ app['id'] }}" class="form-select app-role-new-select"
|
|
data-color="{{ app.color }}">
|
|
<option value="none">— No access</option>
|
|
<option value="standard">👁 Standard — read-only</option>
|
|
<option value="advanced">✏ Advanced — create & edit</option>
|
|
<option value="admin">★ Admin — full access</option>
|
|
</select>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary">Create User</button>
|
|
<a href="{{ url_for('settings.index') }}" class="btn btn-secondary">Cancel</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.querySelectorAll('.app-role-new-select').forEach(function(sel) {
|
|
function refresh() {
|
|
var color = sel.dataset.color;
|
|
if (sel.value !== 'none') {
|
|
sel.style.borderColor = color;
|
|
sel.style.color = color;
|
|
} else {
|
|
sel.style.borderColor = '';
|
|
sel.style.color = '';
|
|
}
|
|
}
|
|
refresh();
|
|
sel.addEventListener('change', refresh);
|
|
});
|
|
</script>
|
|
{% endblock %}
|