Refactor user settings and add user roles management

Reorganized user account settings under a new 'Users' section, replacing the old user accounts route and template with 'users_general' and 'user_roles' management pages. Updated navigation and permissions logic in templates and routes to reflect the new structure. Added placeholder and under development notices to relevant pages, and extended CSS utility classes for UI enhancements.
This commit is contained in:
Christopher
2025-10-01 23:07:54 -06:00
parent 29cad55c60
commit a460189c97
10 changed files with 659 additions and 99 deletions

View File

@@ -522,7 +522,7 @@ def create_app(config_name=None):
from .routes.admin_management import bp as admin_management_bp
app.register_blueprint(admin_management_bp, url_prefix='/admin/settings/admins')
from .routes.role_management import bp as role_management_bp
app.register_blueprint(role_management_bp, url_prefix='/admin/settings/roles')
app.register_blueprint(role_management_bp, url_prefix='/admin/settings/admin/roles')
from .routes.users import bp as users_bp
app.register_blueprint(users_bp, url_prefix='/admin/users')
from .routes.admin_user import admin_user_bp

View File

@@ -32,7 +32,7 @@ def index():
# The first one the user has access to will be their destination.
permission_map = [
('manage_general_settings', 'settings.general'),
('manage_general_settings', 'settings.user_accounts'),
('manage_users_general', 'settings.users_general'),
('view_admins_tab', 'admin_management.index'),
('view_admins_tab', 'role_management.index'), # Use same perm for both admin tabs
('manage_discord_settings', 'settings.discord'),
@@ -99,31 +99,6 @@ def general():
active_tab='general'
)
@bp.route('/user_accounts', methods=['GET', 'POST'])
@login_required
@setup_required
@permission_required('manage_general_settings')
def user_accounts():
# Redirect AppUsers without admin permissions away from admin pages
if current_user.userType == UserType.LOCAL and not current_user.has_permission('manage_general_settings'):
flash('You do not have permission to access the user accounts settings page.', 'danger')
return redirect(url_for('user.index'))
from app.forms import UserAccountsSettingsForm
form = UserAccountsSettingsForm()
if form.validate_on_submit():
Setting.set('ALLOW_USER_ACCOUNTS', form.allow_user_accounts.data, SettingValueType.BOOLEAN, "Allow User Accounts")
log_event(EventType.SETTING_CHANGE, "User account settings updated.", admin_id=current_user.id)
flash('User account settings saved successfully.', 'success')
return redirect(url_for('settings.user_accounts'))
elif request.method == 'GET':
form.allow_user_accounts.data = Setting.get_bool('ALLOW_USER_ACCOUNTS', False)
return render_template(
'settings/index.html',
title="User Account Settings",
form=form,
active_tab='user_accounts'
)
@bp.route('/account', methods=['GET', 'POST'])
@login_required
@@ -746,4 +721,80 @@ def api_debug_execute():
return jsonify({'error': f'Request error: {str(e)}'}), 500
except Exception as e:
current_app.logger.error(f"API Debug error: {e}")
return jsonify({'error': f'Unexpected error: {str(e)}'}), 500
return jsonify({'error': f'Unexpected error: {str(e)}'}), 500
# Users General Management Routes
@bp.route('/users/general', methods=['GET', 'POST'])
@login_required
@setup_required
@permission_required('manage_users_general')
def users_general():
"""Display users general settings page"""
from app.forms import UserAccountsSettingsForm
form = UserAccountsSettingsForm()
if form.validate_on_submit():
# Handle user account settings
Setting.set('ALLOW_USER_ACCOUNTS', form.allow_user_accounts.data, SettingValueType.BOOLEAN, "Allow User Accounts")
log_event(EventType.SETTING_CHANGE, "User account settings updated.", admin_id=current_user.id)
flash('User account settings saved successfully.', 'success')
return redirect(url_for('settings.users_general'))
elif request.method == 'GET':
# Load current settings from database
form.allow_user_accounts.data = Setting.get_bool('ALLOW_USER_ACCOUNTS', False)
return render_template(
'settings/index.html',
title="Users General Settings",
form=form,
active_tab='users_general'
)
# User Roles Management Routes
@bp.route('/users/roles')
@login_required
@setup_required
@permission_required('manage_user_roles')
def user_roles():
"""Display user roles management page"""
# For now, return empty list until user roles model is implemented
user_roles = []
return render_template(
'settings/index.html',
title="User Role Management",
user_roles=user_roles,
active_tab='user_roles'
)
@bp.route('/users/roles/create')
@login_required
@setup_required
@permission_required('create_user_role')
def create_user_role():
"""Create new user role page"""
# Placeholder for future implementation
flash('User role creation is not yet implemented.', 'info')
return redirect(url_for('settings.user_roles'))
@bp.route('/users/roles/<int:role_id>/edit')
@login_required
@setup_required
@permission_required('edit_user_role')
def edit_user_role(role_id):
"""Edit user role page"""
# Placeholder for future implementation
flash('User role editing is not yet implemented.', 'info')
return redirect(url_for('settings.user_roles'))
@bp.route('/users/roles/<int:role_id>/delete', methods=['POST'])
@login_required
@setup_required
@permission_required('delete_user_role')
def delete_user_role(role_id):
"""Delete user role"""
# Placeholder for future implementation
flash('User role deletion is not yet implemented.', 'info')
return redirect(url_for('settings.user_roles'))

View File

@@ -4393,6 +4393,10 @@
.rounded-xl {
border-radius: var(--radius-xl);
}
.rounded-l {
border-top-left-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
}
.border {
border-style: var(--tw-border-style);
border-width: 1px;
@@ -4919,6 +4923,12 @@
background-color: color-mix(in oklab, var(--color-base-300) 70%, transparent);
}
}
.bg-base-content\/20 {
background-color: var(--color-base-content);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-base-content) 20%, transparent);
}
}
.bg-black {
background-color: var(--color-black);
}
@@ -4958,6 +4968,9 @@
background-color: color-mix(in oklab, var(--color-blue-500) 20%, transparent);
}
}
.bg-blue-600 {
background-color: var(--color-blue-600);
}
.bg-blue-600\/20 {
background-color: color-mix(in srgb, oklch(54.6% 0.245 262.881) 20%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -5069,6 +5082,9 @@
.bg-indigo-50 {
background-color: var(--color-indigo-50);
}
.bg-indigo-500 {
background-color: var(--color-indigo-500);
}
.bg-indigo-500\/20 {
background-color: color-mix(in srgb, oklch(58.5% 0.233 277.117) 20%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -5339,6 +5355,9 @@
background-color: color-mix(in oklab, var(--color-secondary) 20%, transparent);
}
}
.bg-slate-500 {
background-color: var(--color-slate-500);
}
.bg-slate-500\/20 {
background-color: color-mix(in srgb, oklch(55.4% 0.046 257.417) 20%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -5612,6 +5631,10 @@
--tw-gradient-to: var(--color-blue-100);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-blue-500 {
--tw-gradient-to: var(--color-blue-500);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-blue-500\/10 {
--tw-gradient-to: color-mix(in srgb, oklch(62.3% 0.214 259.815) 10%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -6681,6 +6704,9 @@
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring-accent {
--tw-ring-color: var(--color-accent);
}
.ring-accent\/40 {
--tw-ring-color: var(--color-accent);
@supports (color: color-mix(in lab, red, red)) {
@@ -6696,6 +6722,9 @@
--tw-ring-color: color-mix(in oklab, var(--color-amber-600) 20%, transparent);
}
}
.ring-audiobookshelf {
--tw-ring-color: var(--color-audiobookshelf);
}
.ring-audiobookshelf-600 {
--tw-ring-color: var(--color-audiobookshelf-600);
}
@@ -6714,6 +6743,9 @@
.ring-base-300 {
--tw-ring-color: var(--color-base-300);
}
.ring-blue-500 {
--tw-ring-color: var(--color-blue-500);
}
.ring-blue-500\/20 {
--tw-ring-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 20%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -6729,6 +6761,9 @@
--tw-ring-color: color-mix(in oklab, var(--color-blue-600) 20%, transparent);
}
}
.ring-emby {
--tw-ring-color: var(--color-emby);
}
.ring-emby-600 {
--tw-ring-color: var(--color-emby-600);
}
@@ -6744,6 +6779,9 @@
--tw-ring-color: color-mix(in oklab, var(--color-emby) 20%, transparent);
}
}
.ring-error {
--tw-ring-color: var(--color-error);
}
.ring-error\/40 {
--tw-ring-color: var(--color-error);
@supports (color: color-mix(in lab, red, red)) {
@@ -6777,12 +6815,18 @@
--tw-ring-color: color-mix(in oklab, var(--color-indigo-600) 20%, transparent);
}
}
.ring-info {
--tw-ring-color: var(--color-info);
}
.ring-info\/40 {
--tw-ring-color: var(--color-info);
@supports (color: color-mix(in lab, red, red)) {
--tw-ring-color: color-mix(in oklab, var(--color-info) 40%, transparent);
}
}
.ring-jellyfin {
--tw-ring-color: var(--color-jellyfin);
}
.ring-jellyfin-600 {
--tw-ring-color: var(--color-jellyfin-600);
}
@@ -6798,6 +6842,9 @@
--tw-ring-color: color-mix(in oklab, var(--color-jellyfin) 20%, transparent);
}
}
.ring-kavita {
--tw-ring-color: var(--color-kavita);
}
.ring-kavita-600 {
--tw-ring-color: var(--color-kavita-600);
}
@@ -6813,6 +6860,9 @@
--tw-ring-color: color-mix(in oklab, var(--color-kavita) 20%, transparent);
}
}
.ring-komga {
--tw-ring-color: var(--color-komga);
}
.ring-komga-600 {
--tw-ring-color: var(--color-komga-600);
}
@@ -6837,6 +6887,9 @@
--tw-ring-color: color-mix(in oklab, var(--color-orange-600) 20%, transparent);
}
}
.ring-plex {
--tw-ring-color: var(--color-plex);
}
.ring-plex-600 {
--tw-ring-color: var(--color-plex-600);
}
@@ -6891,6 +6944,9 @@
--tw-ring-color: color-mix(in oklab, var(--color-red-600) 20%, transparent);
}
}
.ring-romm {
--tw-ring-color: var(--color-romm);
}
.ring-romm-600 {
--tw-ring-color: var(--color-romm-600);
}
@@ -6906,6 +6962,9 @@
--tw-ring-color: color-mix(in oklab, var(--color-romm) 20%, transparent);
}
}
.ring-secondary {
--tw-ring-color: var(--color-secondary);
}
.ring-secondary\/40 {
--tw-ring-color: var(--color-secondary);
@supports (color: color-mix(in lab, red, red)) {
@@ -7376,6 +7435,16 @@
}
}
}
.hover\:border-primary\/30 {
&:hover {
@media (hover: hover) {
border-color: var(--color-primary);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-primary) 30%, transparent);
}
}
}
}
.hover\:bg-\[\#00a4dc\]\/10 {
&:hover {
@media (hover: hover) {

View File

@@ -92,7 +92,24 @@
<ul class="ml-4">
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('manage_general_settings') %}
<li><a href="{{ url_for('settings.general') }}" class="{{ 'active menu-active' if request.endpoint == 'settings.general' else '' }}"><i class="fa-solid fa-sliders fa-fw"></i> General</a></li>
<li><a href="{{ url_for('settings.user_accounts') }}" class="{{ 'active menu-active' if request.endpoint == 'settings.user_accounts' else '' }}"><i class="fa-solid fa-user-plus fa-fw"></i> User Accounts</a></li>
{% endif %}
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('view_users_tab') %}
{% set users_area_active = request.endpoint and (request.endpoint.startswith('settings.users_') or request.endpoint.startswith('settings.user_roles')) %}
<li>
<details {% if users_area_active %}open{% endif %}>
<summary class="{{ 'active' if users_area_active else '' }}">
<i class="fa-solid fa-users fa-fw mr-2"></i> Users
</summary>
<ul class="ml-4">
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('manage_users_general') %}
<li><a href="{{ url_for('settings.users_general') }}" class="{{ 'active menu-active' if request.endpoint == 'settings.users_general' else '' }}"><i class="fa-solid fa-cog mr-2"></i> General</a></li>
{% endif %}
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('manage_user_roles') %}
<li><a href="{{ url_for('settings.user_roles') }}" class="{{ 'active menu-active' if request.endpoint == 'settings.user_roles' else '' }}"><i class="fa-solid fa-user-tag mr-2"></i> Roles</a></li>
{% endif %}
</ul>
</details>
</li>
{% endif %}
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('view_admins_tab') %}
{% set admin_area_active = request.endpoint and (request.endpoint.startswith('admin_management.') or request.endpoint.startswith('role_management.')) %}

View File

@@ -45,6 +45,21 @@
</div>
</div>
<!-- Under Redevelopment Notice -->
<div class="bg-warning/10 border border-warning/20 rounded-lg p-4 mb-6">
<div class="flex items-start gap-3">
<div class="w-6 h-6 rounded-full bg-warning/20 flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fa-solid fa-triangle-exclamation text-warning text-xs"></i>
</div>
<div>
<h4 class="font-medium text-warning mb-1">Under Redevelopment</h4>
<p class="text-sm text-base-content/80">
This page is currently being redeveloped to improve functionality and user experience. Some features may be temporarily unavailable or behave differently than expected.
</p>
</div>
</div>
</div>
<!-- Admins List Section -->
<div class="bg-base-100 border border-base-300 rounded-lg p-6">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">

View File

@@ -17,7 +17,6 @@
<ul>
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('manage_general_settings') %}
<li><a href="{{ url_for('settings.general') }}" class="{{ 'active menu-active' if active_tab == 'general' else '' }}"><i class="fa-solid fa-sliders mr-2"></i> General</a></li>
<li><a href="{{ url_for('settings.user_accounts') }}" class="{{ 'active menu-active' if active_tab == 'user_accounts' else '' }}"><i class="fa-solid fa-user-plus mr-2"></i> User Accounts</a></li>
{% endif %}
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('view_admins_tab') %}
<h2 class="menu-title"><span><i class="fa-solid fa-user-shield fa-fw mr-2"></i> Manage Admins</span></h2>
@@ -30,6 +29,17 @@
{% endif %}
</ul>
{% endif %}
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('view_users_tab') %}
<h2 class="menu-title"><span><i class="fa-solid fa-users fa-fw mr-2"></i> Users</span></h2>
<ul>
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('manage_users_general') %}
<li><a href="{{ url_for('settings.users_general') }}" class="{{ 'active menu-active' if active_tab in ['users_general', 'users_general_edit'] else '' }}"><i class="fa-solid fa-cog mr-2"></i> General</a></li>
{% endif %}
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('manage_user_roles') %}
<li><a href="{{ url_for('settings.user_roles') }}" class="{{ 'active menu-active' if active_tab in ['user_roles', 'user_roles_edit'] else '' }}"><i class="fa-solid fa-user-tag mr-2"></i> Roles</a></li>
{% endif %}
</ul>
{% endif %}
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('manage_discord_settings') %}
<li><a href="{{ url_for('settings.discord') }}" class="{{ 'active menu-active' if active_tab == 'discord' else '' }}"><i class="fa-brands fa-discord mr-2"></i> Discord</a></li>
{% endif %}
@@ -57,7 +67,6 @@
<select class="select select-bordered" onchange="if (this.value) window.location.href=this.value;">
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('manage_general_settings') %}
<option value="{{ url_for('settings.general') }}" {% if active_tab == 'general' %}selected{% endif %}>General</option>
<option value="{{ url_for('settings.user_accounts') }}" {% if active_tab == 'user_accounts' %}selected{% endif %}>User Accounts</option>
{% endif %}
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('create_admin') or current_user.has_permission('edit_admin') or current_user.has_permission('delete_admin') %}
<option value="{{ url_for('admin_management.index') }}" {% if active_tab == 'admins' %}selected{% endif %}>Manage Admins</option>
@@ -65,6 +74,12 @@
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('create_role') or current_user.has_permission('edit_role') or current_user.has_permission('delete_role') %}
<option value="{{ url_for('role_management.index') }}" {% if active_tab == 'roles' %}selected{% endif %}>Manage Roles</option>
{% endif %}
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('manage_users_general') %}
<option value="{{ url_for('settings.users_general') }}" {% if active_tab == 'users_general' %}selected{% endif %}>Users General</option>
{% endif %}
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('manage_user_roles') %}
<option value="{{ url_for('settings.user_roles') }}" {% if active_tab == 'user_roles' %}selected{% endif %}>User Roles</option>
{% endif %}
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('manage_discord_settings') %}
<option value="{{ url_for('settings.discord') }}" {% if active_tab == 'discord' %}selected{% endif %}>Discord</option>
{% endif %}
@@ -94,9 +109,6 @@
{% elif active_tab == 'general' %}
{% include 'settings/general/index.html' %}
{% elif active_tab == 'user_accounts' %}
{% include 'settings/user_accounts/index.html' %}
{% elif active_tab == 'discord' %}
{% include 'settings/discord/index.html' %}
@@ -122,6 +134,14 @@
{% include 'settings/roles/index.html' %}
{% elif active_tab == 'roles_edit' %}
{% include 'settings/roles/edit.html' %}
{% elif active_tab == 'users_general' %}
{% include 'settings/users_general/index.html' %}
{% elif active_tab == 'users_general_edit' %}
{% include 'settings/users_general/edit.html' %}
{% elif active_tab == 'user_roles' %}
{% include 'settings/user_roles/index.html' %}
{% elif active_tab == 'user_roles_edit' %}
{% include 'settings/user_roles/edit.html' %}
{% elif active_tab == 'logs' %}
{% include 'settings/logs/index.html' %}
{% else %}

View File

@@ -15,6 +15,21 @@
</div>
</div>
<!-- Under Redevelopment Notice -->
<div class="bg-warning/10 border border-warning/20 rounded-lg p-4 mb-6">
<div class="flex items-start gap-3">
<div class="w-6 h-6 rounded-full bg-warning/20 flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fa-solid fa-triangle-exclamation text-warning text-xs"></i>
</div>
<div>
<h4 class="font-medium text-warning mb-1">Under Redevelopment</h4>
<p class="text-sm text-base-content/80">
This page is currently being redeveloped to improve functionality and user experience. Some features may be temporarily unavailable or behave differently than expected.
</p>
</div>
</div>
</div>
<!-- Roles Overview Section -->
<div class="bg-base-100 border border-base-300 rounded-lg p-6 mb-6">
<div class="flex items-center gap-3 mb-6">

View File

@@ -1,65 +0,0 @@
<!-- File: app/templates/settings/partials/user_accounts.html -->
<div class="space-y-6">
<div class="flex items-center gap-3 mb-6">
<div class="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center">
<i class="fa-solid fa-user-plus text-primary text-lg"></i>
</div>
<div>
<h1 class="text-2xl font-bold text-base-content mb-1">User Account Settings</h1>
<p class="text-base-content/70 text-sm">Configure user account creation and authentication options</p>
</div>
</div>
</div>
<form method="POST" action="{{ url_for('settings.user_accounts') }}" class="space-y-6">
{{ form.hidden_tag() }}
<div class="bg-base-100 border border-base-300 rounded-lg p-6">
<!-- User Account Creation Section -->
<div class="space-y-6">
<div class="flex items-center gap-3 mb-6">
<div class="w-8 h-8 rounded-full bg-info/20 flex items-center justify-center flex-shrink-0">
<i class="fa-solid fa-users text-info text-sm"></i>
</div>
<div>
<h2 class="text-lg font-semibold text-base-content mb-1">Account Creation</h2>
<p class="text-sm text-base-content/70">Control whether users can create accounts through invites</p>
</div>
</div>
<div class="bg-base-200/30 rounded-lg p-4 border border-base-300 hover:border-base-300/60 transition-colors">
<div class="flex items-center gap-2 mb-2">
<i class="fa-solid fa-user-check text-info text-sm"></i>
<h5 class="font-medium text-base-content">{{ form.allow_user_accounts.label.text }}</h5>
</div>
<label class="label cursor-pointer justify-start p-0">
{{ form.allow_user_accounts(class="toggle toggle-primary mr-3", id="allow_user_accounts") }}
<span class="text-sm text-base-content/80">Allow users to create accounts via invites</span>
</label>
<p class="text-xs text-base-content/60 mt-2">{{ form.allow_user_accounts.description }}</p>
</div>
<!-- Information Card -->
<div class="bg-info/10 border border-info/20 rounded-lg p-4">
<div class="flex items-start gap-3">
<div class="w-6 h-6 rounded-full bg-info/20 flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fa-solid fa-info text-info text-xs"></i>
</div>
<div>
<h4 class="font-medium text-info mb-1">How User Accounts Work</h4>
<p class="text-sm text-base-content/80 leading-relaxed">
When enabled, users who receive invites will be prompted to create a user account before accessing media servers.
This allows them to log in to the application, view their streaming history, and manage their own settings.
When disabled, invites will only provide direct access to media servers without creating application accounts.
</p>
</div>
</div>
</div>
</div>
<!-- Save Button -->
<div class="flex justify-end pt-6 border-t border-base-300 mt-6">
{{ form.submit(class="btn btn-primary h-11") }}
</div>
</div>
</form>

View File

@@ -0,0 +1,233 @@
<!-- File: app/templates/settings/user_roles/index.html -->
<!-- Clean Header -->
<div class="mb-8">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 class="text-2xl font-bold text-base-content mb-2">User Role Management</h1>
<p class="text-base-content/70 text-sm">Create and manage user roles with custom permissions</p>
</div>
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('create_user_role') %}
<a href="{{ url_for('settings.create_user_role') }}" class="btn btn-primary">
<i class="fa-solid fa-plus mr-2"></i> Create New User Role
</a>
{% endif %}
</div>
</div>
<!-- Under Redevelopment Notice -->
<div class="bg-warning/10 border border-warning/20 rounded-lg p-4 mb-6">
<div class="flex items-start gap-3">
<div class="w-6 h-6 rounded-full bg-warning/20 flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fa-solid fa-triangle-exclamation text-warning text-xs"></i>
</div>
<div>
<h4 class="font-medium text-warning mb-1">Under Redevelopment</h4>
<p class="text-sm text-base-content/80">
This page is currently being redeveloped to improve functionality and user experience. Some features may be temporarily unavailable or behave differently than expected.
</p>
</div>
</div>
</div>
<!-- User Roles Overview Section -->
<div class="bg-base-100 border border-base-300 rounded-lg p-6 mb-6">
<div class="flex items-center gap-3 mb-6">
<div class="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center flex-shrink-0">
<i class="fa-solid fa-user-tag text-primary text-lg"></i>
</div>
<div>
<h2 class="text-xl font-semibold text-base-content mb-1">User Roles</h2>
<p class="text-sm text-base-content/70">{{ user_roles|length if user_roles is defined else 0 }} role{{ 's' if (user_roles|length if user_roles is defined else 0) != 1 else '' }} configured</p>
</div>
</div>
<!-- Info Card -->
<div class="bg-info/10 border border-info/20 rounded-lg p-4">
<div class="flex items-start gap-3">
<div class="w-6 h-6 rounded-full bg-info/20 flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fa-solid fa-info text-info text-xs"></i>
</div>
<div>
<h4 class="font-medium text-info mb-1">User Role Guidelines</h4>
<p class="text-sm text-base-content/80">
User roles define what actions regular users can perform in the system. These are different from admin roles and control user-level permissions.
</p>
</div>
</div>
</div>
</div>
<!-- User Roles List Section -->
{% if user_roles is defined and user_roles %}
<div class="bg-base-200/30 border border-base-300 rounded-lg p-6">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-secondary/20 flex items-center justify-center">
<i class="fa-solid fa-list text-secondary text-sm"></i>
</div>
<div>
<h3 class="text-lg font-semibold text-base-content">User Roles List</h3>
</div>
</div>
<!-- Search Bar -->
<div class="form-control w-full sm:w-auto">
<div class="relative">
<input type="text"
placeholder="Search user roles..."
class="input input-bordered w-full sm:w-64 h-10 bg-base-100 border-base-300 focus:border-primary focus:bg-base-100 pl-10 pr-4"
id="userRoleSearch">
<i class="fa-solid fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-base-content/40 text-sm"></i>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr class="border-base-300">
<th class="bg-base-100 text-base-content font-semibold">Role</th>
<th class="bg-base-100 text-base-content font-semibold">Description</th>
<th class="bg-base-100 text-base-content font-semibold">Members</th>
<th class="bg-base-100 text-base-content font-semibold text-right">Actions</th>
</tr>
</thead>
<tbody>
{% for role in user_roles %}
<tr class="hover:bg-base-200/50 border-base-300">
<td class="py-4">
<span class="badge text-sm px-3 py-2" style="background-color: {{ role.color or '#808080' }}; border-color: {{ role.color or '#808080' }}; color: {{ get_text_color_for_bg(role.color) if get_text_color_for_bg is defined else '#ffffff' }};">
{% if role.icon %}
<i class="{{ role.icon }} mr-1"></i>
{% endif %}
{{ role.name }}
</span>
</td>
<td class="py-4">
{% if role.description %}
<span class="text-base-content">{{ role.description }}</span>
{% else %}
<span class="text-base-content/60 italic">No description provided</span>
{% endif %}
</td>
<td class="py-4">
<div class="flex items-center gap-2">
<i class="fa-solid fa-users text-base-content/60 text-xs"></i>
<span class="text-base-content">{{ role.users|length if role.users is defined else 0 }}</span>
</div>
</td>
<td class="py-4 text-right">
<div class="flex items-center justify-end gap-2">
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('edit_user_role') %}
<a href="{{ url_for('settings.edit_user_role', role_id=role.id) }}"
class="btn btn-sm btn-ghost hover:bg-primary/10"
title="Edit User Role">
<i class="fa-solid fa-pen-to-square mr-1"></i>
<span class="hidden md:inline">Edit</span>
</a>
{% else %}
<div class="tooltip tooltip-top" data-tip="You don't have permission to edit user roles">
<button class="btn btn-sm btn-ghost opacity-50 cursor-not-allowed" disabled>
<i class="fa-solid fa-lock mr-1"></i>
<span class="hidden md:inline">Locked</span>
</button>
</div>
{% endif %}
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('delete_user_role') %}
<form action="{{ url_for('settings.delete_user_role', role_id=role.id) }}"
method="POST"
class="inline"
onsubmit="return confirm('Are you sure you want to delete the {{ role.name }} user role? This action cannot be undone.');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit"
class="btn btn-sm btn-ghost text-error hover:bg-error/10"
title="Delete User Role">
<i class="fa-solid fa-trash-can mr-1"></i>
<span class="hidden md:inline">Delete</span>
</button>
</form>
{% else %}
<div class="tooltip tooltip-top" data-tip="You don't have permission to delete user roles">
<button class="btn btn-sm btn-ghost text-error opacity-50 cursor-not-allowed" disabled>
<i class="fa-solid fa-lock mr-1"></i>
<span class="hidden md:inline">Locked</span>
</button>
</div>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<!-- Empty State -->
<div class="bg-base-100 border border-base-300 rounded-lg p-12">
<div class="text-center">
<div class="w-16 h-16 rounded-full bg-base-200 flex items-center justify-center mx-auto mb-4">
<i class="fa-solid fa-user-tag text-base-content/40 text-2xl"></i>
</div>
<h3 class="text-lg font-semibold text-base-content mb-2">No User Roles Created</h3>
<p class="text-sm text-base-content/70 mb-6">
Create your first user role to start organizing user permissions and access control.
</p>
{% if current_user.__class__.__name__ == 'Owner' or current_user.has_permission('create_user_role') %}
<a href="{{ url_for('settings.create_user_role') }}" class="btn btn-primary">
<i class="fa-solid fa-plus mr-2"></i> Create First User Role
</a>
{% endif %}
</div>
</div>
{% endif %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('userRoleSearch');
const tableRows = document.querySelectorAll('tbody tr');
if (searchInput && tableRows.length > 0) {
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase().trim();
tableRows.forEach(row => {
const roleName = row.querySelector('td:first-child .badge')?.textContent?.toLowerCase() || '';
const roleDescription = row.querySelector('td:nth-child(2)')?.textContent?.toLowerCase() || '';
const matches = roleName.includes(searchTerm) || roleDescription.includes(searchTerm);
if (matches || searchTerm === '') {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
// Show/hide "no results" message
const visibleRows = Array.from(tableRows).filter(row => row.style.display !== 'none');
let noResultsRow = document.getElementById('no-search-results');
if (visibleRows.length === 0 && searchTerm !== '') {
if (!noResultsRow) {
noResultsRow = document.createElement('tr');
noResultsRow.id = 'no-search-results';
noResultsRow.innerHTML = `
<td colspan="4" class="text-center py-8">
<div class="text-base-content/60">
<i class="fa-solid fa-search text-2xl mb-2"></i>
<p class="text-sm">No user roles found matching "${searchTerm}"</p>
</div>
</td>
`;
document.querySelector('tbody').appendChild(noResultsRow);
}
} else if (noResultsRow) {
noResultsRow.remove();
}
});
}
});
</script>

View File

@@ -0,0 +1,205 @@
<!-- File: app/templates/settings/users_general/index.html -->
<!-- Clean Header -->
<div class="mb-8">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 class="text-2xl font-bold text-base-content mb-2">Users General Settings</h1>
<p class="text-base-content/70 text-sm">Configure general user management settings and preferences</p>
</div>
</div>
</div>
<!-- Under Development Notice -->
<div class="bg-warning/10 border border-warning/20 rounded-lg p-4 mb-6">
<div class="flex items-start gap-3">
<div class="w-6 h-6 rounded-full bg-warning/20 flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fa-solid fa-triangle-exclamation text-warning text-xs"></i>
</div>
<div>
<h4 class="font-medium text-warning mb-1">Under Development</h4>
<p class="text-sm text-base-content/80">
This page is currently under development. Some settings shown are placeholders and not yet functional, except for the User Accounts section which is fully operational.
</p>
</div>
</div>
</div>
<!-- General Settings Overview Section -->
<div class="bg-base-100 border border-base-300 rounded-lg p-6 mb-6">
<div class="flex items-center gap-3 mb-6">
<div class="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center flex-shrink-0">
<i class="fa-solid fa-users-cog text-primary text-lg"></i>
</div>
<div>
<h2 class="text-xl font-semibold text-base-content mb-1">User Management</h2>
<p class="text-sm text-base-content/70">General configuration for user accounts and settings</p>
</div>
</div>
<!-- Info Card -->
<div class="bg-info/10 border border-info/20 rounded-lg p-4">
<div class="flex items-start gap-3">
<div class="w-6 h-6 rounded-full bg-info/20 flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fa-solid fa-info text-info text-xs"></i>
</div>
<div>
<h4 class="font-medium text-info mb-1">User Settings</h4>
<p class="text-sm text-base-content/80">
Configure general user management settings including default permissions, account policies, and user preferences.
</p>
</div>
</div>
</div>
</div>
<!-- Settings Form Section -->
<div class="bg-base-200/30 border border-base-300 rounded-lg p-6">
<div class="flex items-center gap-3 mb-6">
<div class="w-8 h-8 rounded-full bg-secondary/20 flex items-center justify-center">
<i class="fa-solid fa-cog text-secondary text-sm"></i>
</div>
<div>
<h3 class="text-lg font-semibold text-base-content">Configuration Settings</h3>
</div>
</div>
<!-- Placeholder form content -->
<div class="space-y-6">
<!-- User Accounts Feature Section -->
<div class="bg-base-100 border border-base-300 rounded-lg p-4">
<h4 class="font-medium text-base-content mb-3 flex items-center gap-2">
<i class="fa-solid fa-user-plus text-primary text-sm"></i>
User Accounts
</h4>
<p class="text-sm text-base-content/70 mb-4">
Control whether users can create and manage their own accounts.
</p>
<form method="POST" action="{{ url_for('settings.users_general') }}" class="space-y-4">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<!-- Allow User Accounts Toggle -->
<div class="form-control">
<label class="cursor-pointer flex items-center gap-3 p-3 rounded-lg border border-base-300 hover:border-primary/30 transition-colors">
<input type="checkbox"
name="allow_user_accounts"
class="toggle toggle-primary"
{% if form and form.allow_user_accounts and form.allow_user_accounts.data %}checked{% endif %}>
<div class="flex-1">
<div class="font-medium text-base-content">Allow User Account Creation</div>
<div class="text-sm text-base-content/70 mt-1">
When enabled, users can create their own accounts without admin approval. When disabled, only administrators can create user accounts.
</div>
</div>
</label>
</div>
<!-- Additional Info -->
<div class="bg-info/10 border border-info/20 rounded-lg p-3">
<div class="flex items-start gap-2">
<i class="fa-solid fa-info-circle text-info text-sm mt-0.5"></i>
<div class="text-sm text-base-content/80">
<strong>Note:</strong> User accounts are separate from media service accounts. Users will still need to be invited to media servers separately through the invitation system.
</div>
</div>
</div>
<!-- Save Button -->
<div class="flex justify-end">
<button type="submit" class="btn btn-primary btn-sm">
<i class="fa-solid fa-save mr-2"></i>
Save User Account Settings
</button>
</div>
</form>
</div>
<!-- Account Policies Section -->
<div class="bg-base-100 border border-base-300 rounded-lg p-4">
<h4 class="font-medium text-base-content mb-3 flex items-center gap-2">
<i class="fa-solid fa-shield-alt text-primary text-sm"></i>
Account Policies
</h4>
<p class="text-sm text-base-content/70 mb-4">
Configure account security and behavior policies.
</p>
<div class="space-y-4">
<!-- Password Requirements -->
<div class="form-control">
<label class="label">
<span class="label-text text-sm font-medium">Minimum Password Length</span>
</label>
<input type="number" class="input input-bordered input-sm w-24" value="8" min="6" max="50" disabled>
</div>
<!-- Session Timeout -->
<div class="form-control">
<label class="label">
<span class="label-text text-sm font-medium">Session Timeout (hours)</span>
</label>
<input type="number" class="input input-bordered input-sm w-24" value="24" min="1" max="168" disabled>
</div>
<!-- Account Expiration -->
<div class="form-control">
<label class="label">
<span class="label-text text-sm font-medium">Default Account Expiration (days)</span>
</label>
<input type="number" class="input input-bordered input-sm w-24" value="365" min="1" max="3650" disabled>
<label class="label">
<span class="label-text-alt">Set to 0 for no expiration</span>
</label>
</div>
</div>
</div>
<!-- User Preferences Section -->
<div class="bg-base-100 border border-base-300 rounded-lg p-4">
<h4 class="font-medium text-base-content mb-3 flex items-center gap-2">
<i class="fa-solid fa-sliders text-primary text-sm"></i>
Default User Preferences
</h4>
<p class="text-sm text-base-content/70 mb-4">
Set default preferences for new user accounts.
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Default Theme -->
<div class="form-control">
<label class="label">
<span class="label-text text-sm font-medium">Default Theme</span>
</label>
<select class="select select-bordered select-sm" disabled>
<option>Auto (System)</option>
<option>Light</option>
<option>Dark</option>
</select>
</div>
<!-- Default Language -->
<div class="form-control">
<label class="label">
<span class="label-text text-sm font-medium">Default Language</span>
</label>
<select class="select select-bordered select-sm" disabled>
<option>English</option>
<option>Spanish</option>
<option>French</option>
<option>German</option>
</select>
</div>
</div>
</div>
</div>
<!-- Save Button (disabled for now) -->
<div class="flex items-center justify-end gap-3 mt-6 pt-6 border-t border-base-300">
<button type="button" class="btn btn-primary btn-sm" disabled>
<i class="fa-solid fa-save mr-2"></i>
Save Settings
</button>
</div>
</div>