Добавлен функционал деактивации/реактивации ролей пользователей

This commit is contained in:
2025-12-13 01:25:19 +03:00
parent 6470fb7588
commit a1d77d778a
10 changed files with 666 additions and 60 deletions

View File

@@ -32,13 +32,15 @@ class TenantAdminAccessMiddleware:
if hasattr(connection, 'tenant') and connection.tenant: if hasattr(connection, 'tenant') and connection.tenant:
# Проверяем: это не public схема? # Проверяем: это не public схема?
if connection.tenant.schema_name != 'public': if connection.tenant.schema_name != 'public':
# Если пользователь авторизован, но НЕ суперпользователь - блокируем # Проверяем наличие атрибута user (добавляется AuthenticationMiddleware)
if request.user.is_authenticated and not request.user.is_superuser: if hasattr(request, 'user'):
return HttpResponseForbidden( # Если пользователь авторизован, но НЕ суперпользователь - блокируем
"Доступ запрещен. Только системные администраторы могут " if request.user.is_authenticated and not request.user.is_superuser:
"заходить в админ-панель на поддоменах тенантов. " return HttpResponseForbidden(
"Используйте панель управления тенанта." "Доступ запрещен. Только системные администраторы могут "
) "заходить в админ-панель на поддоменах тенантов. "
"Используйте панель управления тенанта."
)
response = self.get_response(request) response = self.get_response(request)
return response return response

View File

@@ -61,4 +61,131 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Модальное окно с паролем -->
{% if generated_password %}
<div class="modal fade" id="passwordModal" tabindex="-1" aria-labelledby="passwordModalLabel"
aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="passwordModalLabel">
<i class="bi bi-check-circle-fill"></i> Пользователь успешно создан
</h5>
</div>
<div class="modal-body">
<div class="alert alert-warning" role="alert">
<i class="bi bi-exclamation-triangle-fill"></i>
<strong>Важно!</strong> Сохраните этот пароль. Он больше не будет показан.
</div>
<p><strong>Email:</strong> {{ created_user_email }}</p>
<div class="mb-3">
<label class="form-label"><strong>Пароль:</strong></label>
<div class="input-group">
<input type="text" class="form-control form-control-lg font-monospace"
id="generatedPassword" value="{{ generated_password }}" readonly>
<button class="btn btn-outline-primary" type="button"
onclick="copyPassword()">
<i class="bi bi-clipboard"></i> Копировать
</button>
</div>
</div>
<div class="mb-3">
<button class="btn btn-outline-success w-100" type="button"
onclick="copyCredentials()">
<i class="bi bi-clipboard-check"></i> Скопировать логин и пароль
</button>
<div class="form-text text-center mt-2">Скопирует в формате: <code>{{ created_user_email }} / пароль</code></div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="passwordSavedCheck"
onchange="toggleCloseButton()">
<label class="form-check-label" for="passwordSavedCheck">
Я скопировал(а) пароль
</label>
</div>
</div>
<div class="modal-footer">
<a href="{% url 'user_roles:list' %}"
class="btn btn-success disabled"
id="closeModalBtn"
disabled>
Вернуться к списку
</a>
</div>
</div>
</div>
</div>
<script>
function copyPassword() {
const passwordInput = document.getElementById('generatedPassword');
passwordInput.select();
passwordInput.setSelectionRange(0, 99999); // Для мобильных устройств
navigator.clipboard.writeText(passwordInput.value).then(function() {
// Меняем текст кнопки на короткое время
const btn = event.target.closest('button');
const originalHTML = btn.innerHTML;
btn.innerHTML = '<i class="bi bi-check-lg"></i> Скопировано!';
btn.classList.remove('btn-outline-primary');
btn.classList.add('btn-success');
setTimeout(function() {
btn.innerHTML = originalHTML;
btn.classList.remove('btn-success');
btn.classList.add('btn-outline-primary');
}, 2000);
});
}
function copyCredentials() {
const email = '{{ created_user_email }}';
const password = '{{ generated_password }}';
const credentials = `${email} / ${password}`;
navigator.clipboard.writeText(credentials).then(function() {
const btn = event.target.closest('button');
const originalHTML = btn.innerHTML;
btn.innerHTML = '<i class="bi bi-check-lg"></i> Скопировано!';
btn.classList.remove('btn-outline-success');
btn.classList.add('btn-success');
setTimeout(function() {
btn.innerHTML = originalHTML;
btn.classList.remove('btn-success');
btn.classList.add('btn-outline-success');
}, 2000);
});
}
function toggleCloseButton() {
const checkbox = document.getElementById('passwordSavedCheck');
const closeBtn = document.getElementById('closeModalBtn');
if (checkbox.checked) {
closeBtn.classList.remove('disabled');
closeBtn.removeAttribute('disabled');
} else {
closeBtn.classList.add('disabled');
closeBtn.setAttribute('disabled', 'disabled');
}
}
// Автоматически показываем модальное окно при загрузке страницы
document.addEventListener('DOMContentLoaded', function() {
const modalElement = document.getElementById('passwordModal');
if (modalElement) {
const passwordModal = new bootstrap.Modal(modalElement);
passwordModal.show();
console.log('Password modal shown');
} else {
console.error('Password modal element not found');
}
});
</script>
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,117 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Пользователь создан{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card border-success">
<div class="card-header bg-success text-white">
<h4 class="mb-0">
<i class="bi bi-check-circle"></i> Пользователь успешно создан
</h4>
</div>
<div class="card-body">
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle"></i>
<strong>Важно!</strong> Сохраните эти данные - пароль больше не будет показан.
</div>
<div class="mb-4">
<h5>Данные для входа:</h5>
<table class="table table-bordered">
<tbody>
<tr>
<th width="30%">Email (логин):</th>
<td>
<code id="userEmail" class="fs-5">{{ created_user_email }}</code>
<button onclick="copyEmail()" class="btn btn-sm btn-outline-secondary ms-2">
<i class="bi bi-clipboard"></i> Копировать
</button>
</td>
</tr>
<tr>
<th>Пароль:</th>
<td>
<code id="userPassword" class="fs-5 text-danger">{{ generated_password }}</code>
<button onclick="copyPassword()" class="btn btn-sm btn-outline-secondary ms-2">
<i class="bi bi-clipboard"></i> Копировать
</button>
</td>
</tr>
<tr>
<th>Роль:</th>
<td><span class="badge bg-primary">{{ user_role }}</span></td>
</tr>
</tbody>
</table>
</div>
<div class="mb-4">
<button onclick="copyCredentials()" class="btn btn-primary btn-lg">
<i class="bi bi-clipboard-check"></i> Копировать логин и пароль
</button>
<small class="text-muted d-block mt-2">
Формат: <code>email / password</code>
</small>
</div>
<div class="d-grid gap-2">
<a href="{% url 'user_roles:list' %}" class="btn btn-success">
<i class="bi bi-list"></i> Перейти к списку пользователей
</a>
<a href="{% url 'user_roles:create' %}" class="btn btn-outline-secondary">
<i class="bi bi-plus-circle"></i> Создать еще одного пользователя
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function copyEmail() {
const email = document.getElementById('userEmail').textContent;
navigator.clipboard.writeText(email).then(function() {
showCopyFeedback('Email скопирован!');
});
}
function copyPassword() {
const password = document.getElementById('userPassword').textContent;
navigator.clipboard.writeText(password).then(function() {
showCopyFeedback('Пароль скопирован!');
});
}
function copyCredentials() {
const email = document.getElementById('userEmail').textContent;
const password = document.getElementById('userPassword').textContent;
const credentials = `${email} / ${password}`;
navigator.clipboard.writeText(credentials).then(function() {
showCopyFeedback('Логин и пароль скопированы!');
});
}
function showCopyFeedback(message) {
// Создаем временное уведомление
const alert = document.createElement('div');
alert.className = 'alert alert-success alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3';
alert.style.zIndex = '9999';
alert.innerHTML = `
<i class="bi bi-check-circle"></i> ${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
// Автоматически удаляем через 3 секунды
setTimeout(() => {
alert.remove();
}, 3000);
}
</script>
{% endblock %}

View File

@@ -1,19 +1,19 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Удалить пользователя{% endblock %} {% block title %}Деактивировать пользователя{% endblock %}
{% block content %} {% block content %}
<div class="container mt-4"> <div class="container mt-4">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<div class="card border-danger"> <div class="card border-warning">
<div class="card-header bg-danger text-white"> <div class="card-header bg-warning">
<h4 class="mb-0">Удалить доступ пользователя</h4> <h4 class="mb-0">Деактивировать пользователя</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="alert alert-warning"> <div class="alert alert-warning">
<i class="bi bi-exclamation-triangle"></i> <i class="bi bi-exclamation-triangle"></i>
<strong>Внимание!</strong> Вы собираетесь удалить доступ пользователя к системе. <strong>Внимание!</strong> Пользователь будет деактивирован, но его данные останутся в системе.
</div> </div>
<dl class="row"> <dl class="row">
@@ -40,16 +40,17 @@
</dl> </dl>
<p class="text-muted"> <p class="text-muted">
Пользователь больше не сможет войти в систему через этот магазин. Пользователь больше не сможет войти в систему.
Учетная запись пользователя не будет удалена. Все его данные (заказы, история действий) сохранятся.
При необходимости вы сможете реактивировать пользователя.
</p> </p>
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<a href="{% url 'user_roles:list' %}" class="btn btn-secondary">Отмена</a> <a href="{% url 'user_roles:list' %}" class="btn btn-secondary">Отмена</a>
<button type="submit" class="btn btn-danger"> <button type="submit" class="btn btn-warning">
<i class="bi bi-trash"></i> Да, удалить доступ <i class="bi bi-x-circle"></i> Да, деактивировать
</button> </button>
</div> </div>
</form> </form>

View File

@@ -21,12 +21,20 @@
{% endif %} {% endif %}
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Пользователь:</label> <label class="form-label">Email:</label>
<p class="form-control-plaintext">{{ user_role.user.email }} ({{ user_role.user.name }})</p> <p class="form-control-plaintext">{{ user_role.user.email }}</p>
</div> </div>
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="action" value="update">
<div class="mb-3">
<label for="name" class="form-label">Имя *</label>
<input type="text" class="form-control" id="name" name="name"
value="{{ user_role.user.name }}" required>
<div class="form-text">Полное имя пользователя</div>
</div>
<div class="mb-3"> <div class="mb-3">
<label for="role" class="form-label">Роль *</label> <label for="role" class="form-label">Роль *</label>
@@ -50,14 +58,167 @@
<div class="form-text">Снимите галочку, чтобы временно отключить доступ</div> <div class="form-text">Снимите галочку, чтобы временно отключить доступ</div>
</div> </div>
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between align-items-center">
<a href="{% url 'user_roles:list' %}" class="btn btn-secondary">Отмена</a> <a href="{% url 'user_roles:list' %}" class="btn btn-secondary">Отмена</a>
<button type="submit" class="btn btn-primary">Сохранить изменения</button> <div>
<button type="button" class="btn btn-warning me-2"
data-bs-toggle="modal" data-bs-target="#regeneratePasswordModal">
<i class="bi bi-key-fill"></i> Пересоздать пароль
</button>
<button type="submit" class="btn btn-primary">Сохранить изменения</button>
</div>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<!-- Модальное окно для подтверждения пересоздания пароля -->
<div class="modal fade" id="regeneratePasswordModal" tabindex="-1" aria-labelledby="regeneratePasswordModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-warning">
<h5 class="modal-title" id="regeneratePasswordModalLabel">
<i class="bi bi-exclamation-triangle-fill"></i> Пересоздать пароль?
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Вы уверены, что хотите создать новый пароль для пользователя <strong>{{ user_role.user.email }}</strong>?</p>
<p class="text-muted mb-0">Текущий пароль будет сброшен и больше не будет работать.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<form method="post" style="display: inline;">
{% csrf_token %}
<input type="hidden" name="action" value="regenerate_password">
<button type="submit" class="btn btn-warning">Да, пересоздать пароль</button>
</form>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Модальное окно с новым паролем -->
{% if generated_password %}
<div class="modal fade" id="passwordModal" tabindex="-1" aria-labelledby="passwordModalLabel"
aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="passwordModalLabel">
<i class="bi bi-check-circle-fill"></i> Пароль успешно пересоздан
</h5>
</div>
<div class="modal-body">
<div class="alert alert-warning" role="alert">
<i class="bi bi-exclamation-triangle-fill"></i>
<strong>Важно!</strong> Сохраните этот пароль. Он больше не будет показан.
</div>
<p><strong>Email:</strong> {{ user_role.user.email }}</p>
<div class="mb-3">
<label class="form-label"><strong>Новый пароль:</strong></label>
<div class="input-group">
<input type="text" class="form-control form-control-lg font-monospace"
id="generatedPassword" value="{{ generated_password }}" readonly>
<button class="btn btn-outline-primary" type="button"
onclick="copyPassword()">
<i class="bi bi-clipboard"></i> Копировать
</button>
</div>
</div>
<div class="mb-3">
<button class="btn btn-outline-success w-100" type="button"
onclick="copyCredentials()">
<i class="bi bi-clipboard-check"></i> Скопировать логин и пароль
</button>
<div class="form-text text-center mt-2">Скопирует в формате: <code>{{ user_role.user.email }} / пароль</code></div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="passwordSavedCheck"
onchange="toggleCloseButton()">
<label class="form-check-label" for="passwordSavedCheck">
Я скопировал(а) пароль
</label>
</div>
</div>
<div class="modal-footer">
<a href="{% url 'user_roles:edit' user_role.pk %}"
class="btn btn-success disabled"
id="closeModalBtn"
disabled>
Продолжить редактирование
</a>
</div>
</div>
</div>
</div>
<script>
function copyPassword() {
const passwordInput = document.getElementById('generatedPassword');
passwordInput.select();
passwordInput.setSelectionRange(0, 99999); // Для мобильных устройств
navigator.clipboard.writeText(passwordInput.value).then(function() {
// Меняем текст кнопки на короткое время
const btn = event.target.closest('button');
const originalHTML = btn.innerHTML;
btn.innerHTML = '<i class="bi bi-check-lg"></i> Скопировано!';
btn.classList.remove('btn-outline-primary');
btn.classList.add('btn-success');
setTimeout(function() {
btn.innerHTML = originalHTML;
btn.classList.remove('btn-success');
btn.classList.add('btn-outline-primary');
}, 2000);
});
}
function copyCredentials() {
const email = '{{ user_role.user.email }}';
const password = '{{ generated_password }}';
const credentials = `${email} / ${password}`;
navigator.clipboard.writeText(credentials).then(function() {
const btn = event.target.closest('button');
const originalHTML = btn.innerHTML;
btn.innerHTML = '<i class="bi bi-check-lg"></i> Скопировано!';
btn.classList.remove('btn-outline-success');
btn.classList.add('btn-success');
setTimeout(function() {
btn.innerHTML = originalHTML;
btn.classList.remove('btn-success');
btn.classList.add('btn-outline-success');
}, 2000);
});
}
function toggleCloseButton() {
const checkbox = document.getElementById('passwordSavedCheck');
const closeBtn = document.getElementById('closeModalBtn');
if (checkbox.checked) {
closeBtn.classList.remove('disabled');
closeBtn.removeAttribute('disabled');
} else {
closeBtn.classList.add('disabled');
closeBtn.setAttribute('disabled', 'disabled');
}
}
// Автоматически показываем модальное окно при загрузке страницы
document.addEventListener('DOMContentLoaded', function() {
const passwordModal = new bootstrap.Modal(document.getElementById('passwordModal'));
passwordModal.show();
});
</script>
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -20,6 +20,25 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<!-- Фильтр по активности -->
<div class="card mb-3">
<div class="card-body">
<form method="get" class="row g-3 align-items-center">
<div class="col-auto">
<div class="form-check">
<input class="form-check-input" type="checkbox"
id="show_inactive" name="show_inactive" value="true"
{% if show_inactive %}checked{% endif %}
onchange="this.form.submit()">
<label class="form-check-label" for="show_inactive">
Показать неактивных пользователей
</label>
</div>
</div>
</form>
</div>
</div>
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
{% if user_roles %} {% if user_roles %}
@@ -52,10 +71,10 @@
</span> </span>
</td> </td>
<td> <td>
{% if user_role.is_active %} {% if not user_role.is_active or not user_role.user.is_active %}
<span class="badge bg-success">Активен</span>
{% else %}
<span class="badge bg-secondary">Неактивен</span> <span class="badge bg-secondary">Неактивен</span>
{% else %}
<span class="badge bg-success">Активен</span>
{% endif %} {% endif %}
</td> </td>
<td>{{ user_role.created_at|date:"d.m.Y H:i" }}</td> <td>{{ user_role.created_at|date:"d.m.Y H:i" }}</td>
@@ -68,12 +87,18 @@
</td> </td>
<td> <td>
{% if user_role.user != request.user %} {% if user_role.user != request.user %}
<a href="{% url 'user_roles:edit' user_role.pk %}" class="btn btn-sm btn-outline-primary"> {% if user_role.is_active and user_role.user.is_active %}
<i class="bi bi-pencil"></i> Изменить <a href="{% url 'user_roles:edit' user_role.pk %}" class="btn btn-sm btn-outline-primary">
</a> <i class="bi bi-pencil"></i> Изменить
<a href="{% url 'user_roles:delete' user_role.pk %}" class="btn btn-sm btn-outline-danger"> </a>
<i class="bi bi-trash"></i> Удалить <a href="{% url 'user_roles:delete' user_role.pk %}" class="btn btn-sm btn-outline-danger">
</a> <i class="bi bi-x-circle"></i> Деактивировать
</a>
{% else %}
<a href="{% url 'user_roles:reactivate' user_role.pk %}" class="btn btn-sm btn-outline-success">
<i class="bi bi-arrow-clockwise"></i> Реактивировать
</a>
{% endif %}
{% else %} {% else %}
<span class="text-muted small"> <span class="text-muted small">
<i class="bi bi-lock"></i> Ваша роль <i class="bi bi-lock"></i> Ваша роль

View File

@@ -0,0 +1,61 @@
{% extends "base.html" %}
{% block title %}Реактивировать пользователя{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card border-success">
<div class="card-header bg-success text-white">
<h4 class="mb-0">Реактивировать пользователя</h4>
</div>
<div class="card-body">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
<strong>Информация:</strong> Пользователь снова получит доступ к системе.
</div>
<dl class="row">
<dt class="col-sm-4">Email:</dt>
<dd class="col-sm-8">{{ user_role.user.email }}</dd>
<dt class="col-sm-4">Имя:</dt>
<dd class="col-sm-8">{{ user_role.user.name }}</dd>
<dt class="col-sm-4">Роль:</dt>
<dd class="col-sm-8">
<span class="badge
{% if user_role.role.code == 'owner' %}bg-danger
{% elif user_role.role.code == 'manager' %}bg-primary
{% elif user_role.role.code == 'florist' %}bg-success
{% elif user_role.role.code == 'courier' %}bg-info
{% endif %}">
{{ user_role.role.name }}
</span>
</dd>
<dt class="col-sm-4">Был деактивирован:</dt>
<dd class="col-sm-8">{{ user_role.created_at|date:"d.m.Y H:i" }}</dd>
</dl>
<p class="text-muted">
Пользователь сможет снова войти в систему с прежней ролью.
Все его данные (заказы, история действий) сохранены.
</p>
<form method="post">
{% csrf_token %}
<div class="d-flex justify-content-between">
<a href="{% url 'user_roles:list' %}" class="btn btn-secondary">Отмена</a>
<button type="submit" class="btn btn-success">
<i class="bi bi-arrow-clockwise"></i> Да, реактивировать
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -8,4 +8,5 @@ urlpatterns = [
path('create/', views.user_role_create, name='create'), path('create/', views.user_role_create, name='create'),
path('<int:pk>/edit/', views.user_role_edit, name='edit'), path('<int:pk>/edit/', views.user_role_edit, name='edit'),
path('<int:pk>/delete/', views.user_role_delete, name='delete'), path('<int:pk>/delete/', views.user_role_delete, name='delete'),
path('<int:pk>/reactivate/', views.user_role_reactivate, name='reactivate'),
] ]

View File

@@ -13,12 +13,24 @@ User = get_user_model()
@owner_required @owner_required
def user_role_list(request): def user_role_list(request):
"""Список пользователей с их ролями""" """Список пользователей с их ролями"""
user_roles = UserRole.objects.select_related('user', 'role', 'created_by').all() # Фильтр по активности
show_inactive = request.GET.get('show_inactive', 'false') == 'true'
if show_inactive:
user_roles = UserRole.objects.select_related('user', 'role', 'created_by').all()
else:
# По умолчанию показываем только активных
user_roles = UserRole.objects.select_related('user', 'role', 'created_by').filter(
is_active=True,
user__is_active=True
)
roles = Role.objects.all() roles = Role.objects.all()
context = { context = {
'user_roles': user_roles, 'user_roles': user_roles,
'roles': roles, 'roles': roles,
'show_inactive': show_inactive,
} }
return render(request, 'user_roles/user_role_list.html', context) return render(request, 'user_roles/user_role_list.html', context)
@@ -34,21 +46,52 @@ def user_role_create(request):
password = request.POST.get('password', User.objects.make_random_password(12)) password = request.POST.get('password', User.objects.make_random_password(12))
try: try:
# Создаем пользователя # Проверяем, не существует ли уже пользователь с таким email
user = User.objects.create_user( existing_user = User.objects.filter(email=email).first()
email=email,
name=name,
password=password,
is_email_confirmed=True,
is_staff=False, # SECURITY: Пользователи ролей НЕ имеют доступа к админке
is_superuser=False # SECURITY: Пользователи ролей НЕ суперпользователи
)
# Назначаем роль if existing_user:
RoleService.assign_role_to_user(user, role_code, created_by=request.user) # Пользователь существует - проверяем его статус
if not existing_user.is_active:
# Пользователь деактивирован - предлагаем реактивировать
messages.warning(
request,
f'Пользователь {email} уже существует, но деактивирован. '
f'Используйте функцию "Реактивировать" в списке пользователей.'
)
else:
# Пользователь активен - возможно, уже имеет роль в другом тенанте
messages.error(
request,
f'Пользователь с email {email} уже существует в системе.'
)
# ВАЖНО: Возвращаемся к форме, чтобы пользователь мог исправить email
roles = Role.objects.all()
context = {'roles': roles}
return render(request, 'user_roles/user_role_create.html', context)
else:
# Создаем нового пользователя
user = User.objects.create_user(
email=email,
name=name,
password=password,
is_email_confirmed=True,
is_staff=False, # SECURITY: Пользователи ролей НЕ имеют доступа к админке
is_superuser=False # SECURITY: Пользователи ролей НЕ суперпользователи
)
messages.success(request, f'Пользователь {email} создан с ролью {role_code}. Пароль: {password}') # Назначаем роль
return redirect('user_roles:list') RoleService.assign_role_to_user(user, role_code, created_by=request.user)
# DEBUG: Проверяем, что пароль есть
print(f"DEBUG: created_user_email={email}, generated_password={password}")
# Показываем страницу с паролем
context = {
'created_user_email': email,
'generated_password': password,
'user_role': role_code,
}
return render(request, 'user_roles/user_role_created.html', context)
except Exception as e: except Exception as e:
messages.error(request, f'Ошибка при создании пользователя: {str(e)}') messages.error(request, f'Ошибка при создании пользователя: {str(e)}')
@@ -73,24 +116,55 @@ def user_role_edit(request, pk):
return redirect('user_roles:list') return redirect('user_roles:list')
if request.method == 'POST': if request.method == 'POST':
role_code = request.POST.get('role') action = request.POST.get('action', 'update')
is_active = request.POST.get('is_active') == 'on'
try: # Обработка пересоздания пароля
# Обновляем роль if action == 'regenerate_password':
role = RoleService.get_role_by_code(role_code) try:
if not role: new_password = User.objects.make_random_password(12)
raise ValueError(f"Роль '{role_code}' не найдена") user_role.user.set_password(new_password)
user_role.user.save()
user_role.role = role messages.success(request, f'Пароль для пользователя {user_role.user.email} успешно обновлен')
user_role.is_active = is_active
user_role.save()
messages.success(request, f'Роль пользователя {user_role.user.email} обновлена') # Показываем новый пароль в модальном окне
return redirect('user_roles:list') roles = Role.objects.all()
context = {
'user_role': user_role,
'roles': roles,
'generated_password': new_password,
}
return render(request, 'user_roles/user_role_edit.html', context)
except Exception as e: except Exception as e:
messages.error(request, f'Ошибка при обновлении роли: {str(e)}') messages.error(request, f'Ошибка при обновлении пароля: {str(e)}')
# Обычное обновление роли и имени
else:
role_code = request.POST.get('role')
is_active = request.POST.get('is_active') == 'on'
name = request.POST.get('name', '').strip()
try:
# Обновляем роль
role = RoleService.get_role_by_code(role_code)
if not role:
raise ValueError(f"Роль '{role_code}' не найдена")
user_role.role = role
user_role.is_active = is_active
user_role.save()
# Обновляем имя пользователя
if name and name != user_role.user.name:
user_role.user.name = name
user_role.user.save()
messages.success(request, f'Данные пользователя {user_role.user.email} обновлены')
return redirect('user_roles:list')
except Exception as e:
messages.error(request, f'Ошибка при обновлении данных: {str(e)}')
roles = Role.objects.all() roles = Role.objects.all()
context = { context = {
@@ -103,7 +177,7 @@ def user_role_edit(request, pk):
@login_required @login_required
@owner_required @owner_required
def user_role_delete(request, pk): def user_role_delete(request, pk):
"""Удаление роли пользователя (отключение доступа)""" """Деактивация пользователя (soft delete)"""
user_role = get_object_or_404(UserRole, pk=pk) user_role = get_object_or_404(UserRole, pk=pk)
# Защита от самоблокировки # Защита от самоблокировки
@@ -114,11 +188,43 @@ def user_role_delete(request, pk):
if request.method == 'POST': if request.method == 'POST':
email = user_role.user.email email = user_role.user.email
user_role.delete()
messages.success(request, f'Доступ пользователя {email} удален') # Soft delete: деактивируем пользователя и его роль
user_role.is_active = False
user_role.save()
user_role.user.is_active = False
user_role.user.save()
messages.success(request, f'Пользователь {email} деактивирован')
return redirect('user_roles:list') return redirect('user_roles:list')
context = { context = {
'user_role': user_role, 'user_role': user_role,
} }
return render(request, 'user_roles/user_role_delete.html', context) return render(request, 'user_roles/user_role_delete.html', context)
@login_required
@owner_required
def user_role_reactivate(request, pk):
"""Реактивация пользователя"""
user_role = get_object_or_404(UserRole, pk=pk)
if request.method == 'POST':
email = user_role.user.email
# Реактивируем пользователя и его роль
user_role.is_active = True
user_role.save()
user_role.user.is_active = True
user_role.user.save()
messages.success(request, f'Пользователь {email} успешно реактивирован')
return redirect('user_roles:list')
context = {
'user_role': user_role,
}
return render(request, 'user_roles/user_role_reactivate.html', context)

View File

@@ -42,3 +42,8 @@ client = Client.objects.get(schema_name='public')
with schema_context(client): with schema_context(client):
call_command('createsuperuser') call_command('createsuperuser')
exit() exit()
# 7. Восстановление стандартных ролей для конкретного тенанта
# Если роли были удалены или нужно их пересоздать:
python manage.py tenant_command init_roles --schema=anatol
# Где 'anatol' - имя схемы тенанта (schema_name)