updated settings

This commit is contained in:
2025-07-17 16:01:10 +03:00
parent 52e4daf37f
commit e37cbf9fee
5 changed files with 343 additions and 181 deletions

View File

@@ -30,6 +30,16 @@ class User(db.Model, UserMixin):
"""Check if user has admin role"""
return self.role == 'admin'
@property
def is_super_admin(self):
"""Check if user has super admin role"""
return self.role == 'sadmin'
@property
def has_admin_access(self):
"""Check if user has any admin access (admin or super admin)"""
return self.role in ['admin', 'sadmin']
@property
def is_active(self):
"""Required by Flask-Login"""

View File

@@ -13,15 +13,25 @@ import os
bp = Blueprint('admin', __name__)
def admin_required(f):
"""Decorator to require admin role"""
"""Decorator to require admin or super admin role"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or not current_user.is_admin:
if not current_user.is_authenticated or not current_user.has_admin_access:
flash('Admin access required.', 'danger')
return redirect(url_for('dashboard.index'))
return f(*args, **kwargs)
return decorated_function
def super_admin_required(f):
"""Decorator to require super admin role only"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or not current_user.is_super_admin:
flash('Super admin access required.', 'danger')
return redirect(url_for('dashboard.index'))
return f(*args, **kwargs)
return decorated_function
@bp.route('/')
@login_required
@admin_required
@@ -64,10 +74,15 @@ def create_user():
flash('Password must be at least 6 characters long.', 'danger')
return redirect(url_for('admin.index'))
if role not in ['user', 'admin']:
if role not in ['user', 'admin', 'sadmin']:
flash('Invalid role specified.', 'danger')
return redirect(url_for('admin.index'))
# Prevent creating sadmin users - sadmin only exists from deployment
if role == 'sadmin':
flash('Super admin users cannot be created through the interface.', 'danger')
return redirect(url_for('admin.index'))
# Check if user already exists
if User.query.filter_by(username=username).first():
flash(f'User "{username}" already exists.', 'danger')
@@ -91,7 +106,7 @@ def create_user():
@bp.route('/delete_user', methods=['POST'])
@login_required
@admin_required
@super_admin_required
def delete_user():
"""Delete a user using POST form data"""
user_id = request.form.get('user_id')
@@ -107,6 +122,11 @@ def delete_user():
user = User.query.get_or_404(user_id)
username = user.username
# Prevent deletion of sadmin users - they are permanent
if user.role == 'sadmin':
flash('Super admin users cannot be deleted.', 'danger')
return redirect(url_for('admin.index'))
try:
db.session.delete(user)
db.session.commit()
@@ -122,9 +142,9 @@ def delete_user():
@bp.route('/change_role/<int:user_id>', methods=['POST'])
@login_required
@admin_required
@super_admin_required
def change_role(user_id):
"""Change user role"""
"""Change user role - restricted to super admin"""
# Prevent changing own role
if user_id == current_user.id:
flash('You cannot change your own role.', 'danger')
@@ -133,10 +153,20 @@ def change_role(user_id):
user = User.query.get_or_404(user_id)
new_role = request.form.get('role')
if new_role not in ['user', 'admin']:
if new_role not in ['user', 'admin', 'sadmin']:
flash('Invalid role specified.', 'danger')
return redirect(url_for('admin.index'))
# Prevent any changes to sadmin users - they are permanent
if user.role == 'sadmin':
flash('Super admin users cannot have their role changed.', 'danger')
return redirect(url_for('admin.index'))
# Prevent assigning sadmin role - sadmin only exists from deployment
if new_role == 'sadmin':
flash('Super admin role cannot be assigned through the interface.', 'danger')
return redirect(url_for('admin.index'))
try:
old_role = user.role
user.role = new_role
@@ -423,10 +453,20 @@ def edit_user():
flash('Username cannot be empty.', 'danger')
return redirect(url_for('admin.index'))
if role not in ['user', 'admin']:
if role not in ['user', 'admin', 'sadmin']:
flash('Invalid role specified.', 'danger')
return redirect(url_for('admin.index'))
# Prevent changing sadmin users - they are permanent
if user.role == 'sadmin':
flash('Super admin users cannot be modified.', 'danger')
return redirect(url_for('admin.index'))
# Prevent assigning sadmin role - sadmin only exists from deployment
if role == 'sadmin':
flash('Super admin role cannot be assigned through the interface.', 'danger')
return redirect(url_for('admin.index'))
# Check if username is taken by another user
if username != user.username:
existing_user = User.query.filter_by(username=username).first()

View File

@@ -2,12 +2,47 @@
{% block title %}Admin Panel - SKE Digital Signage{% endblock %}
{% block extra_css %}
<style>
.clickable-row:hover {
background-color: #f8f9fa !important;
}
.expand-icon {
transition: transform 0.3s ease;
}
.expand-icon.rotated {
transform: rotate(180deg);
}
.edit-row {
background-color: #f8f9fa;
border-left: 4px solid #007bff;
}
.edit-row td {
padding: 0;
}
.clickable-row {
transition: background-color 0.2s ease;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Page Header -->
<div class="row mb-4">
<div class="col">
<h1><i class="bi bi-gear-fill"></i> Admin Panel</h1>
<h1><i class="bi bi-gear-fill"></i> Admin Panel
{% if current_user.is_super_admin %}
<span class="badge bg-danger ms-2">Super Admin</span>
{% elif current_user.is_admin %}
<span class="badge bg-warning ms-2">Admin</span>
{% endif %}
</h1>
<p class="text-muted">System administration and user management</p>
</div>
</div>
@@ -125,7 +160,8 @@
</thead>
<tbody>
{% for user in users %}
<tr>
<!-- Main user row (clickable) -->
<tr class="user-row {% if user.username != current_user.username and user.role != 'sadmin' %}clickable-row{% endif %}" data-user-id="{{ user.id }}" {% if user.username != current_user.username and user.role != 'sadmin' %}style="cursor: pointer;"{% endif %}>
<td>
<strong>{{ user.username }}</strong>
{% if user.username == current_user.username %}
@@ -133,8 +169,10 @@
{% endif %}
</td>
<td>
{% if user.role == 'admin' %}
<span class="badge bg-danger">Admin</span>
{% if user.role == 'sadmin' %}
<span class="badge bg-danger">Super Admin</span>
{% elif user.role == 'admin' %}
<span class="badge bg-warning">Admin</span>
{% else %}
<span class="badge bg-primary">User</span>
{% endif %}
@@ -149,28 +187,86 @@
<td>{{ user.created_at.strftime('%Y-%m-%d') if user.created_at else 'N/A' }}</td>
<td>{{ user.last_login.strftime('%Y-%m-%d %H:%M') if user.last_login else 'Never' }}</td>
<td>
<div class="btn-group btn-group-sm">
{% if user.username != current_user.username %}
<button type="button" class="btn btn-outline-warning edit-user-btn"
data-user-id="{{ user.id }}"
data-username="{{ user.username }}"
data-role="{{ user.role }}"
data-active="{{ user.is_active_user|tojson }}">
<i class="bi bi-pencil"></i>
{% if user.username != current_user.username and user.role != 'sadmin' %}
<i class="bi bi-chevron-down expand-icon"></i>
<small class="text-muted">Click to edit</small>
{% elif user.role == 'sadmin' %}
<span class="badge bg-danger">Protected</span>
{% else %}
<span class="badge bg-secondary">Current User</span>
{% endif %}
</td>
</tr>
<!-- Expandable edit row (hidden by default) -->
{% if user.username != current_user.username and user.role != 'sadmin' %}
<tr class="edit-row" id="edit-row-{{ user.id }}" style="display: none;">
<td colspan="6" class="bg-light">
<div class="row p-3">
<div class="col-md-8">
<h6><i class="bi bi-pencil"></i> Edit User: {{ user.username }}</h6>
<form method="POST" action="{{ url_for('admin.edit_user') }}" class="row g-3">
<input type="hidden" name="user_id" value="{{ user.id }}">
<div class="col-md-4">
<label class="form-label">Username</label>
<input type="text" class="form-control form-control-sm" name="username" value="{{ user.username }}" required>
</div>
<div class="col-md-4">
<label class="form-label">New Password</label>
<input type="password" class="form-control form-control-sm" name="password" placeholder="Leave blank to keep current">
</div>
<div class="col-md-2">
<label class="form-label">Role</label>
<select name="role" class="form-select form-select-sm">
<option value="user" {% if user.role == 'user' %}selected{% endif %}>User</option>
<option value="admin" {% if user.role == 'admin' %}selected{% endif %}>Admin</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Status</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active" {% if user.is_active_user %}checked{% endif %}>
<label class="form-check-label">Active</label>
</div>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary btn-sm me-2">
<i class="bi bi-check-lg"></i> Save Changes
</button>
<button type="button" class="btn btn-secondary btn-sm collapse-row" data-user-id="{{ user.id }}">
<i class="bi bi-x-lg"></i> Cancel
</button>
</div>
</form>
</div>
<div class="col-md-4">
{% if current_user.is_super_admin %}
<h6 class="text-danger"><i class="bi bi-trash"></i> Delete User</h6>
<p class="small text-muted">This action cannot be undone.</p>
<form method="POST" action="{{ url_for('admin.delete_user') }}" onsubmit="return confirm('Are you sure you want to delete user {{ user.username }}? This action cannot be undone!');">
<input type="hidden" name="user_id" value="{{ user.id }}">
<button type="submit" class="btn btn-danger btn-sm">
<i class="bi bi-trash"></i> Delete User
</button>
</form>
{% else %}
<h6 class="text-muted"><i class="bi bi-shield-lock"></i> Delete User</h6>
<p class="small text-muted">Super admin access required.</p>
<button type="button" class="btn btn-outline-secondary btn-sm" disabled>
<i class="bi bi-shield-lock"></i> Restricted
</button>
<button type="button" class="btn btn-outline-danger delete-user-btn"
data-user-id="{{ user.id }}"
data-username="{{ user.username }}">
<i class="bi bi-trash"></i>
</button>
{% else %}
<button type="button" class="btn btn-outline-secondary" disabled>
<i class="bi bi-lock"></i>
</button>
{% endif %}
{% endif %}
</div>
</div>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
@@ -493,73 +589,6 @@
</div>
</div>
<!-- Edit User Modal -->
<div class="modal fade" id="editUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST" action="{{ url_for('admin.edit_user') }}" onsubmit="setTimeout(function(){ window.location.reload(); }, 1000);">
<input type="hidden" id="editUserId" name="user_id">
<div class="modal-body">
<div class="mb-3">
<label for="editUsername" class="form-label">Username</label>
<input type="text" class="form-control" id="editUsername" name="username" required>
</div>
<div class="mb-3">
<label for="editRole" class="form-label">Role</label>
<select class="form-select" id="editRole" name="role">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="editIsActive" name="is_active">
<label class="form-check-label" for="editIsActive">
Active User
</label>
</div>
</div>
<div class="mb-3">
<label for="editPassword" class="form-label">New Password (leave blank to keep current)</label>
<input type="password" class="form-control" id="editPassword" name="password">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Update User</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete User Modal -->
<div class="modal fade" id="deleteUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete user <strong id="deleteUsername"></strong>?</p>
<p class="text-danger small">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<form method="POST" action="{{ url_for('admin.delete_user') }}" style="display:inline;" onsubmit="setTimeout(function(){ window.location.reload(); }, 1000);">
<input type="hidden" id="deleteUserId" name="user_id">
<button type="submit" class="btn btn-danger">Delete User</button>
</form>
</div>
</div>
</div>
</div>
<!-- Quick Schedule Task Modal -->
<div class="modal fade" id="quickScheduleModal" tabindex="-1">
<div class="modal-dialog">
@@ -614,70 +643,6 @@
</div>
<script>
let currentUserId = null;
function editUser(userId, username, role, isActive) {
console.log('editUser called with:', userId, username, role, isActive);
currentUserId = userId;
const editUserId = document.getElementById('editUserId');
const editUsername = document.getElementById('editUsername');
const editRole = document.getElementById('editRole');
const editIsActive = document.getElementById('editIsActive');
if (!editUserId || !editUsername || !editRole || !editIsActive) {
console.error('Modal elements not found');
return;
}
editUserId.value = userId;
editUsername.value = username;
editRole.value = role;
editIsActive.checked = isActive;
document.getElementById('editPassword').value = '';
try {
const modalElement = document.getElementById('editUserModal');
if (!modalElement) {
console.error('Edit modal not found');
return;
}
const modal = new bootstrap.Modal(modalElement);
modal.show();
console.log('Modal should be shown');
} catch (error) {
console.error('Error showing modal:', error);
}
}
function deleteUser(userId, username) {
console.log('deleteUser called with:', userId, username);
const deleteUserId = document.getElementById('deleteUserId');
const deleteUsername = document.getElementById('deleteUsername');
if (!deleteUserId || !deleteUsername) {
console.error('Delete modal elements not found');
return;
}
deleteUserId.value = userId;
deleteUsername.textContent = username;
try {
const modalElement = document.getElementById('deleteUserModal');
if (!modalElement) {
console.error('Delete modal not found');
return;
}
const modal = new bootstrap.Modal(modalElement);
modal.show();
console.log('Delete modal should be shown');
} catch (error) {
console.error('Error showing delete modal:', error);
}
}
function clearLogs() {
if (confirm('Are you sure you want to clear all server logs? This action cannot be undone.')) {
fetch('/admin/clear_logs', {
@@ -916,30 +881,158 @@ function deleteTask(taskId) {
document.addEventListener('DOMContentLoaded', function() {
console.log('Admin page loaded - initializing...');
// Add event listeners for edit and delete buttons
document.querySelectorAll('.edit-user-btn').forEach(button => {
button.addEventListener('click', function() {
// Debug: Check how many clickable rows we found
const clickableRows = document.querySelectorAll('.clickable-row');
console.log('Found clickable rows:', clickableRows.length);
if (clickableRows.length === 0) {
console.warn('No clickable rows found! Check if users are loaded and rendered.');
// Check if we're on the right tab
setTimeout(function() {
const activeTab = document.querySelector('.nav-link.active');
console.log('Active tab:', activeTab ? activeTab.textContent : 'none');
const userTable = document.querySelector('#users table');
console.log('User table found:', !!userTable);
if (userTable) {
const rows = userTable.querySelectorAll('tbody tr');
console.log('Total table rows found:', rows.length);
}
}, 1000);
}
// Add event listeners for expandable user rows
clickableRows.forEach((row, index) => {
console.log(`Setting up click handler for row ${index}:`, row);
row.addEventListener('click', function(e) {
console.log('Row clicked!', e.target);
// Don't expand if clicking on a form element
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') {
console.log('Clicked on form element, ignoring');
return;
}
const userId = this.getAttribute('data-user-id');
const username = this.getAttribute('data-username');
const role = this.getAttribute('data-role');
const isActive = this.getAttribute('data-active') === 'true';
console.log('Edit button clicked:', userId, username, role, isActive);
editUser(userId, username, role, isActive);
console.log('User ID:', userId);
const editRow = document.getElementById(`edit-row-${userId}`);
console.log('Edit row found:', !!editRow);
const expandIcon = this.querySelector('.expand-icon');
console.log('Expand icon found:', !!expandIcon);
if (editRow) {
if (editRow.style.display === 'none' || editRow.style.display === '') {
// Close all other edit rows first
document.querySelectorAll('.edit-row').forEach(row => {
row.style.display = 'none';
});
document.querySelectorAll('.expand-icon').forEach(icon => {
icon.classList.remove('rotated');
});
// Open this edit row
editRow.style.display = 'table-row';
if (expandIcon) expandIcon.classList.add('rotated');
console.log('Opened edit row for user:', userId);
} else {
// Close this edit row
editRow.style.display = 'none';
if (expandIcon) expandIcon.classList.remove('rotated');
console.log('Closed edit row for user:', userId);
}
} else {
console.error('Edit row not found for user:', userId);
}
});
});
document.querySelectorAll('.delete-user-btn').forEach(button => {
// Add event listeners for collapse buttons
document.querySelectorAll('.collapse-row').forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-user-id');
const username = this.getAttribute('data-username');
console.log('Delete button clicked:', userId, username);
deleteUser(userId, username);
const editRow = document.getElementById(`edit-row-${userId}`);
const userRow = document.querySelector(`.clickable-row[data-user-id="${userId}"]`);
const expandIcon = userRow ? userRow.querySelector('.expand-icon') : null;
if (editRow) {
editRow.style.display = 'none';
if (expandIcon) expandIcon.classList.remove('rotated');
console.log('Collapsed edit row for user:', userId);
}
});
});
// Make functions globally accessible
window.editUser = editUser;
window.deleteUser = deleteUser;
// Setup click handlers for user rows (also call this when tabs change)
function setupUserRowClickHandlers() {
console.log('Setting up user row click handlers...');
// Remove any existing handlers to avoid duplicates
document.querySelectorAll('.clickable-row').forEach(row => {
row.replaceWith(row.cloneNode(true));
});
// Add fresh handlers
document.querySelectorAll('.clickable-row').forEach((row, index) => {
console.log(`Setting up click handler for row ${index}:`, row);
row.addEventListener('click', function(e) {
console.log('Row clicked!', e.target, 'Row:', this);
// Don't expand if clicking on a form element
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') {
console.log('Clicked on form element, ignoring');
return;
}
const userId = this.getAttribute('data-user-id');
console.log('User ID:', userId);
const editRow = document.getElementById(`edit-row-${userId}`);
console.log('Edit row found:', !!editRow);
const expandIcon = this.querySelector('.expand-icon');
console.log('Expand icon found:', !!expandIcon);
if (editRow) {
if (editRow.style.display === 'none' || editRow.style.display === '') {
// Close all other edit rows first
document.querySelectorAll('.edit-row').forEach(row => {
row.style.display = 'none';
});
document.querySelectorAll('.expand-icon').forEach(icon => {
icon.classList.remove('rotated');
});
// Open this edit row
editRow.style.display = 'table-row';
if (expandIcon) expandIcon.classList.add('rotated');
console.log('Opened edit row for user:', userId);
} else {
// Close this edit row
editRow.style.display = 'none';
if (expandIcon) expandIcon.classList.remove('rotated');
console.log('Closed edit row for user:', userId);
}
} else {
console.error('Edit row not found for user:', userId);
}
});
});
}
// Call setup initially
setupUserRowClickHandlers();
// Re-setup handlers when users tab is shown
const usersTab = document.getElementById('users-tab');
if (usersTab) {
usersTab.addEventListener('shown.bs.tab', function() {
console.log('Users tab shown, re-setting up click handlers...');
setTimeout(setupUserRowClickHandlers, 100); // Small delay to ensure content is rendered
});
}
// Check for successful user operations and handle refresh
checkForUserOperationSuccess();
@@ -972,6 +1065,9 @@ document.addEventListener('DOMContentLoaded', function() {
console.log('Maintenance tab content found:', !!maintenanceContent);
console.log('Scheduler tab content found:', !!schedulerContent);
});
// Frequency change handler for schedule preview
document.getElementById('quickFrequency').addEventListener('change', function() {
const frequency = this.value;
const timeInput = document.getElementById('quickTime');
const previewText = document.getElementById('schedulePreview');
@@ -1081,6 +1177,6 @@ function checkForUserOperationSuccess() {
}, 2000); // Wait 2 seconds to let user read the success message
}
}
}
</script>
{% endblock %}

View File

@@ -2,5 +2,3 @@
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_localhost FALSE / FALSE 0 session .eJwlzj0OwjAMQOG7ZGZw4jixe5nKfxGsLZ0Qd6cSw1ufvk_Z15Hns2zv48pH2V9RtmKAo6OyUqXUrCBjpNEUCTBpbHVpcmgaGCii3S0koxUzQYJm12bIHqgLPQktrI8ejaP5PSXEMbuzAU83ndFFbWlzl0ia5YZcZx5_TS3fHxMgMKY.aHenRQ.0uMtLODE40iqcA-M96_kh2ZMGTQ
#HttpOnly_127.0.0.1 FALSE / FALSE 0 session .eJydUdtqwzAM_ZWg5zAcX2I7_7GntQTJktdAdiF2YKX032fKHgctfRCSQEfnHOkCc16xnKTA9HaBrrYEZU9JSoEeXots3QGqlLq3chZeqvABuv2bsVXd32je1_X8Asdrf3eHbmiWVZ5DN3Da5Enq-eOLl7w8ZoDx8122fxXg2iTwuZOfpdTyIPfd0x379otNygmmuu3SuoVhAlJmtAYDusEJyqDiOAo5HyMrijrQkFECo5AihcZQi2wcucxeVGTnLWoyIbHBbJI4Q0x2tKwD69SWOmNGb1MgFXwi9GwjUkadUmRxvlmZbwZuaga4_gKYh7s4.aHeshg.sXfRf_q0gHSntT7w8BMVXvOARAs

20
main.py
View File

@@ -37,7 +37,7 @@ def create_default_admin():
admin_username = os.environ.get('ADMIN_USER', 'admin')
admin_password = os.environ.get('ADMIN_PASSWORD', 'admin123')
# Check if admin user already exists
# Create default admin user
if not User.query.filter_by(username=admin_username).first():
hashed_password = bcrypt.generate_password_hash(admin_password).decode('utf-8')
admin_user = User(
@@ -51,6 +51,24 @@ def create_default_admin():
log_action(f"Default admin user '{admin_username}' created")
else:
print(f"Admin user '{admin_username}' already exists")
# Create default super admin user
sadmin_username = os.environ.get('SADMIN_USER', 'sadmin')
sadmin_password = os.environ.get('SADMIN_PASSWORD', 'sadmin123')
if not User.query.filter_by(username=sadmin_username).first():
hashed_password = bcrypt.generate_password_hash(sadmin_password).decode('utf-8')
sadmin_user = User(
username=sadmin_username,
password=hashed_password,
role='sadmin'
)
db.session.add(sadmin_user)
db.session.commit()
print(f"Default super admin user '{sadmin_username}' created with password '{sadmin_password}'")
log_action(f"Default super admin user '{sadmin_username}' created")
else:
print(f"Super admin user '{sadmin_username}' already exists")
if __name__ == '__main__':
main()