snapshot: preserve role management and user lifecycle controls
This commit is contained in:
@@ -58,6 +58,7 @@ TEMPLATES = [
|
|||||||
'django.template.context_processors.request',
|
'django.template.context_processors.request',
|
||||||
'django.contrib.auth.context_processors.auth',
|
'django.contrib.auth.context_processors.auth',
|
||||||
'django.contrib.messages.context_processors.messages',
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
'workflows.context_processors.role_context',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -4,3 +4,6 @@ from django.apps import AppConfig
|
|||||||
class WorkflowsConfig(AppConfig):
|
class WorkflowsConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'workflows'
|
name = 'workflows'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from . import signals # noqa: F401
|
||||||
|
|||||||
5
backend/workflows/context_processors.py
Normal file
5
backend/workflows/context_processors.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from .roles import template_role_context
|
||||||
|
|
||||||
|
|
||||||
|
def role_context(request):
|
||||||
|
return template_role_context(getattr(request, 'user', None))
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import get_language, gettext as _
|
from django.utils.translation import get_language, gettext as _
|
||||||
|
|
||||||
from .form_builder import apply_form_field_config
|
from .form_builder import apply_form_field_config
|
||||||
from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, WorkflowConfig
|
from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, WorkflowConfig
|
||||||
|
from .roles import ROLE_ADMIN, ROLE_GROUP_NAMES, ROLE_IT_STAFF, ROLE_LABELS, ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role
|
||||||
|
|
||||||
|
|
||||||
YES_NO_CHOICES = [('', '--'), ('ja', 'Ja'), ('nein', 'Nein')]
|
YES_NO_CHOICES = [('', '--'), ('ja', 'Ja'), ('nein', 'Nein')]
|
||||||
@@ -96,6 +98,60 @@ HARDWARE_EXTRA_CHOICES = [('Smartphone', 'Smartphone'), ('Anderes', 'Anderes')]
|
|||||||
SOFTWARE_EXTRA_CHOICES = [('Adobe Acrobat Pro (Abonnement: Zusätzliche Kosten)', 'Adobe Acrobat Pro (Abonnement: Zusätzliche Kosten)'), ('Anderes', 'Anderes')]
|
SOFTWARE_EXTRA_CHOICES = [('Adobe Acrobat Pro (Abonnement: Zusätzliche Kosten)', 'Adobe Acrobat Pro (Abonnement: Zusätzliche Kosten)'), ('Anderes', 'Anderes')]
|
||||||
|
|
||||||
|
|
||||||
|
class UserManagementCreateForm(forms.Form):
|
||||||
|
first_name = forms.CharField(label=_('Vorname'), max_length=150, required=False)
|
||||||
|
last_name = forms.CharField(label=_('Nachname'), max_length=150, required=False)
|
||||||
|
username = forms.CharField(label=_('Benutzername'), max_length=150)
|
||||||
|
email = forms.EmailField(label=_('E-Mail-Adresse'))
|
||||||
|
role_key = forms.ChoiceField(label=_('Rolle'))
|
||||||
|
password1 = forms.CharField(label=_('Passwort'), widget=forms.PasswordInput())
|
||||||
|
password2 = forms.CharField(label=_('Passwort bestätigen'), widget=forms.PasswordInput())
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['role_key'].choices = [
|
||||||
|
(role_key, str(ROLE_LABELS[role_key]))
|
||||||
|
for role_key in (ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF)
|
||||||
|
]
|
||||||
|
|
||||||
|
def clean_username(self):
|
||||||
|
username = (self.cleaned_data.get('username') or '').strip()
|
||||||
|
user_model = get_user_model()
|
||||||
|
if user_model.objects.filter(username=username).exists():
|
||||||
|
raise forms.ValidationError(_('Dieser Benutzername ist bereits vergeben.'))
|
||||||
|
return username
|
||||||
|
|
||||||
|
def clean_email(self):
|
||||||
|
return (self.cleaned_data.get('email') or '').strip().lower()
|
||||||
|
|
||||||
|
def clean_role_key(self):
|
||||||
|
role_key = (self.cleaned_data.get('role_key') or '').strip()
|
||||||
|
if role_key not in ROLE_GROUP_NAMES:
|
||||||
|
raise forms.ValidationError(_('Ungültige Rolle.'))
|
||||||
|
return role_key
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned = super().clean()
|
||||||
|
password1 = cleaned.get('password1')
|
||||||
|
password2 = cleaned.get('password2')
|
||||||
|
if password1 and password2 and password1 != password2:
|
||||||
|
self.add_error('password2', _('Die Passwörter stimmen nicht überein.'))
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
user_model = get_user_model()
|
||||||
|
user = user_model.objects.create_user(
|
||||||
|
username=self.cleaned_data['username'],
|
||||||
|
email=self.cleaned_data['email'],
|
||||||
|
password=self.cleaned_data['password1'],
|
||||||
|
first_name=self.cleaned_data.get('first_name', ''),
|
||||||
|
last_name=self.cleaned_data.get('last_name', ''),
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
assign_user_role(user, self.cleaned_data['role_key'])
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
class OnboardingRequestForm(forms.ModelForm):
|
class OnboardingRequestForm(forms.ModelForm):
|
||||||
first_name = forms.CharField(label='Vorname', required=False)
|
first_name = forms.CharField(label='Vorname', required=False)
|
||||||
last_name = forms.CharField(label='Nachname', required=False)
|
last_name = forms.CharField(label='Nachname', required=False)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from workflows.roles import ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role, ensure_role_groups
|
||||||
|
|
||||||
DEFAULT_USERS = [
|
DEFAULT_USERS = [
|
||||||
{
|
{
|
||||||
@@ -43,6 +44,8 @@ class Command(BaseCommand):
|
|||||||
is_staff=item['is_staff'],
|
is_staff=item['is_staff'],
|
||||||
is_superuser=item['is_superuser'],
|
is_superuser=item['is_superuser'],
|
||||||
)
|
)
|
||||||
|
ensure_role_groups()
|
||||||
|
assign_user_role(user, ROLE_SUPER_ADMIN if item['username'] == 'admin_test' else ROLE_STAFF)
|
||||||
self.stdout.write(f'created {user.username}')
|
self.stdout.write(f'created {user.username}')
|
||||||
|
|
||||||
self.stdout.write(self.style.SUCCESS('initial users created'))
|
self.stdout.write(self.style.SUCCESS('initial users created'))
|
||||||
|
|||||||
127
backend/workflows/roles.py
Normal file
127
backend/workflows/roles.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
ROLE_SUPER_ADMIN = 'super_admin'
|
||||||
|
ROLE_ADMIN = 'admin'
|
||||||
|
ROLE_IT_STAFF = 'it_staff'
|
||||||
|
ROLE_STAFF = 'staff'
|
||||||
|
|
||||||
|
ROLE_GROUP_NAMES = {
|
||||||
|
ROLE_SUPER_ADMIN: 'Super Admin',
|
||||||
|
ROLE_ADMIN: 'Admin',
|
||||||
|
ROLE_IT_STAFF: 'IT Staff',
|
||||||
|
ROLE_STAFF: 'Staff',
|
||||||
|
}
|
||||||
|
|
||||||
|
ROLE_LABELS = {
|
||||||
|
ROLE_SUPER_ADMIN: _('Super Admin'),
|
||||||
|
ROLE_ADMIN: _('Admin'),
|
||||||
|
ROLE_IT_STAFF: _('IT Staff'),
|
||||||
|
ROLE_STAFF: _('Mitarbeiter'),
|
||||||
|
}
|
||||||
|
|
||||||
|
CAPABILITIES = {
|
||||||
|
'manage_users': {ROLE_SUPER_ADMIN},
|
||||||
|
'access_requests_dashboard': {ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF},
|
||||||
|
'run_intro_session': {ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF},
|
||||||
|
'generate_intro_pdfs': {ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF},
|
||||||
|
'retry_requests': {ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF},
|
||||||
|
'delete_requests': {ROLE_SUPER_ADMIN, ROLE_ADMIN},
|
||||||
|
'manage_integrations': {ROLE_SUPER_ADMIN, ROLE_ADMIN},
|
||||||
|
'manage_welcome_emails': {ROLE_SUPER_ADMIN, ROLE_ADMIN},
|
||||||
|
'manage_builders': {ROLE_SUPER_ADMIN, ROLE_ADMIN},
|
||||||
|
'view_audit_log': {ROLE_SUPER_ADMIN, ROLE_ADMIN},
|
||||||
|
'manage_backups': {ROLE_SUPER_ADMIN, ROLE_ADMIN},
|
||||||
|
'view_docs': {ROLE_SUPER_ADMIN, ROLE_ADMIN},
|
||||||
|
'access_django_admin_link': {ROLE_SUPER_ADMIN},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_role_groups() -> None:
|
||||||
|
for name in ROLE_GROUP_NAMES.values():
|
||||||
|
Group.objects.get_or_create(name=name)
|
||||||
|
|
||||||
|
|
||||||
|
def assign_user_role(user, role_key: str) -> None:
|
||||||
|
ensure_role_groups()
|
||||||
|
if role_key not in ROLE_GROUP_NAMES:
|
||||||
|
raise ValueError(f'Unknown role: {role_key}')
|
||||||
|
|
||||||
|
role_groups = Group.objects.filter(name__in=ROLE_GROUP_NAMES.values())
|
||||||
|
user.groups.remove(*role_groups)
|
||||||
|
user.groups.add(Group.objects.get(name=ROLE_GROUP_NAMES[role_key]))
|
||||||
|
|
||||||
|
is_super_admin = role_key == ROLE_SUPER_ADMIN
|
||||||
|
user.is_staff = is_super_admin
|
||||||
|
user.is_superuser = is_super_admin
|
||||||
|
user.save(update_fields=['is_staff', 'is_superuser'])
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_bootstrap_role_assignments() -> None:
|
||||||
|
user_model = get_user_model()
|
||||||
|
bootstrap_roles = {
|
||||||
|
'admin_test': ROLE_SUPER_ADMIN,
|
||||||
|
'user_test': ROLE_STAFF,
|
||||||
|
}
|
||||||
|
role_group_names = set(ROLE_GROUP_NAMES.values())
|
||||||
|
for username, role_key in bootstrap_roles.items():
|
||||||
|
try:
|
||||||
|
user = user_model.objects.get(username=username)
|
||||||
|
except user_model.DoesNotExist:
|
||||||
|
continue
|
||||||
|
if user.groups.filter(name__in=role_group_names).exists():
|
||||||
|
continue
|
||||||
|
assign_user_role(user, role_key)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_role_key(user) -> str:
|
||||||
|
if not getattr(user, 'is_authenticated', False):
|
||||||
|
return ROLE_STAFF
|
||||||
|
if getattr(user, 'is_superuser', False):
|
||||||
|
return ROLE_SUPER_ADMIN
|
||||||
|
|
||||||
|
group_names = set(user.groups.values_list('name', flat=True))
|
||||||
|
for role_key in (ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF):
|
||||||
|
if ROLE_GROUP_NAMES[role_key] in group_names:
|
||||||
|
return role_key
|
||||||
|
|
||||||
|
if getattr(user, 'is_staff', False):
|
||||||
|
return ROLE_ADMIN
|
||||||
|
return ROLE_STAFF
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_role_label(user) -> str:
|
||||||
|
return str(ROLE_LABELS[get_user_role_key(user)])
|
||||||
|
|
||||||
|
|
||||||
|
def user_has_capability(user, capability: str) -> bool:
|
||||||
|
if not getattr(user, 'is_authenticated', False):
|
||||||
|
return False
|
||||||
|
if getattr(user, 'is_superuser', False):
|
||||||
|
return True
|
||||||
|
allowed_roles = CAPABILITIES.get(capability, set())
|
||||||
|
return get_user_role_key(user) in allowed_roles
|
||||||
|
|
||||||
|
|
||||||
|
def template_role_context(user) -> dict[str, object]:
|
||||||
|
role_key = get_user_role_key(user)
|
||||||
|
return {
|
||||||
|
'role_key': role_key,
|
||||||
|
'role_label': str(ROLE_LABELS[role_key]),
|
||||||
|
'can_manage_users': user_has_capability(user, 'manage_users'),
|
||||||
|
'can_access_requests_dashboard': user_has_capability(user, 'access_requests_dashboard'),
|
||||||
|
'can_run_intro_session': user_has_capability(user, 'run_intro_session'),
|
||||||
|
'can_generate_intro_pdfs': user_has_capability(user, 'generate_intro_pdfs'),
|
||||||
|
'can_retry_requests': user_has_capability(user, 'retry_requests'),
|
||||||
|
'can_delete_requests': user_has_capability(user, 'delete_requests'),
|
||||||
|
'can_manage_integrations': user_has_capability(user, 'manage_integrations'),
|
||||||
|
'can_manage_welcome_emails': user_has_capability(user, 'manage_welcome_emails'),
|
||||||
|
'can_manage_builders': user_has_capability(user, 'manage_builders'),
|
||||||
|
'can_view_audit_log': user_has_capability(user, 'view_audit_log'),
|
||||||
|
'can_manage_backups': user_has_capability(user, 'manage_backups'),
|
||||||
|
'can_view_docs': user_has_capability(user, 'view_docs'),
|
||||||
|
'can_access_django_admin_link': user_has_capability(user, 'access_django_admin_link'),
|
||||||
|
}
|
||||||
12
backend/workflows/signals.py
Normal file
12
backend/workflows/signals.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from django.db.models.signals import post_migrate
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from .roles import ensure_bootstrap_role_assignments, ensure_role_groups
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_migrate)
|
||||||
|
def workflows_post_migrate(sender, **kwargs):
|
||||||
|
if getattr(sender, 'name', '') != 'workflows':
|
||||||
|
return
|
||||||
|
ensure_role_groups()
|
||||||
|
ensure_bootstrap_role_assignments()
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
body { margin: 0; font-family: Arial, sans-serif; background: #f4f8ff; color: #0f172a; padding: 20px; }
|
body { margin: 0; font-family: Arial, sans-serif; background: #f4f8ff; color: #0f172a; padding: 20px; }
|
||||||
[hidden] { display: none !important; }
|
[hidden] { display: none !important; }
|
||||||
|
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
||||||
.shell { max-width: 1100px; margin: 0 auto; background: #fff; border: 1px solid #d8e3f0; border-radius: 14px; padding: 16px; }
|
.shell { max-width: 1100px; margin: 0 auto; background: #fff; border: 1px solid #d8e3f0; border-radius: 14px; padding: 16px; }
|
||||||
h1 { margin: 12px 0 6px; color: #000078; }
|
h1 { margin: 12px 0 6px; color: #000078; }
|
||||||
.sub { margin: 0 0 12px; color: #54657c; }
|
.sub { margin: 0 0 12px; color: #54657c; }
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
<li><code>/backend/config/</code>: Django settings, WSGI, URL config</li>
|
<li><code>/backend/config/</code>: Django settings, WSGI, URL config</li>
|
||||||
<li><code>/backend/workflows/</code>: application logic, views, models, tasks, templates, static assets</li>
|
<li><code>/backend/workflows/</code>: application logic, views, models, tasks, templates, static assets</li>
|
||||||
<li><code>/backend/workflows/templates/workflows/base_shell.html</code>: standard page shell for new staff-facing pages</li>
|
<li><code>/backend/workflows/templates/workflows/base_shell.html</code>: standard page shell for new staff-facing pages</li>
|
||||||
|
<li><code>/backend/workflows/roles.py</code>: centralized role names, capability matrix, and template permission helpers</li>
|
||||||
<li>Rule: all interactive app pages should extend <code>base_shell.html</code>; do not rebuild topbar/frame logic in page-local templates.</li>
|
<li>Rule: all interactive app pages should extend <code>base_shell.html</code>; do not rebuild topbar/frame logic in page-local templates.</li>
|
||||||
<li><code>/backend/media/templates/</code>: PDF HTML templates and letterhead source files</li>
|
<li><code>/backend/media/templates/</code>: PDF HTML templates and letterhead source files</li>
|
||||||
<li><code>/backend/media/pdfs/</code>: generated PDF outputs on host volume</li>
|
<li><code>/backend/media/pdfs/</code>: generated PDF outputs on host volume</li>
|
||||||
@@ -99,6 +100,19 @@ docker compose exec -T web python manage.py check</code></pre>
|
|||||||
<li>Fresh boot sequence runs migrations automatically in <code>entrypoint-web.sh</code>.</li>
|
<li>Fresh boot sequence runs migrations automatically in <code>entrypoint-web.sh</code>.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<h3>Role and Permission Model</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Stable Django group names: <code>Super Admin</code>, <code>Admin</code>, <code>IT Staff</code>, <code>Staff</code>.</li>
|
||||||
|
<li>Groups are created automatically through a <code>post_migrate</code> hook in <code>workflows.signals</code>.</li>
|
||||||
|
<li>Capability checks are centralized in <code>workflows.roles.CAPABILITIES</code>.</li>
|
||||||
|
<li>Use <code>_require_capability(...)</code> in views instead of flat <code>is_staff</code> checks.</li>
|
||||||
|
<li>Templates receive permission flags from <code>workflows.context_processors.role_context</code>.</li>
|
||||||
|
<li>Super-admin-only user management lives at <code>/admin-tools/users/</code> and is the preferred path for normal role assignment, account activation, password-reset mail dispatch, and controlled user deletion.</li>
|
||||||
|
<li>Backward-compatibility rule: authenticated legacy users with <code>is_staff=True</code> but no explicit role group currently fall back to the <code>Admin</code> capability set.</li>
|
||||||
|
<li><code>superuser</code> accounts resolve to <code>Super Admin</code>.</li>
|
||||||
|
<li>When adding a new operational page or action, define the capability in <code>roles.py</code>, gate the view, and hide the UI affordance when the capability is absent.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<h2 id="translations">6) Translation Workflow</h2>
|
<h2 id="translations">6) Translation Workflow</h2>
|
||||||
<h3>Standard Django i18n path</h3>
|
<h3>Standard Django i18n path</h3>
|
||||||
<pre><code>make i18n-update-en
|
<pre><code>make i18n-update-en
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
<h1>{% trans "TUBCO Onboarding & Offboarding Portal" %}</h1>
|
<h1>{% trans "TUBCO Onboarding & Offboarding Portal" %}</h1>
|
||||||
<p>{% trans "Zentrale Arbeitsfläche für Anfragen, PDF-Generierung, E-Mail-Workflows und Ablage in Nextcloud." %}</p>
|
<p>{% trans "Zentrale Arbeitsfläche für Anfragen, PDF-Generierung, E-Mail-Workflows und Ablage in Nextcloud." %}</p>
|
||||||
<div class="status-row">
|
<div class="status-row">
|
||||||
<span class="status-pill status-pill-neutral">{% trans "Rolle:" %} {% if request.user.is_staff %}{% trans "Admin" %}{% else %}{% trans "Mitarbeiter" %}{% endif %}</span>
|
<span class="status-pill status-pill-neutral">{% trans "Rolle:" %} {{ role_label }}</span>
|
||||||
<span class="status-pill {% if nextcloud_enabled %}ok{% else %}warn{% endif %}">
|
<span class="status-pill {% if nextcloud_enabled %}ok{% else %}warn{% endif %}">
|
||||||
{% trans "Nextcloud:" %} {% if nextcloud_enabled %}{% trans "aktiv" %}{% else %}{% trans "inaktiv" %}{% endif %}
|
{% trans "Nextcloud:" %} {% if nextcloud_enabled %}{% trans "aktiv" %}{% else %}{% trans "inaktiv" %}{% endif %}
|
||||||
</span>
|
</span>
|
||||||
@@ -88,6 +88,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{% if can_access_requests_dashboard %}
|
||||||
<section class="app-card">
|
<section class="app-card">
|
||||||
<div>
|
<div>
|
||||||
<div class="top-line"><div class="accent">APP</div></div>
|
<div class="top-line"><div class="accent">APP</div></div>
|
||||||
@@ -103,34 +104,51 @@
|
|||||||
<a class="btn btn-secondary" href="/requests/">{% trans "Dashboard öffnen" %}</a>
|
<a class="btn btn-secondary" href="/requests/">{% trans "Dashboard öffnen" %}</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if request.user.is_staff %}
|
{% if can_manage_users or can_manage_integrations or can_view_audit_log or can_manage_backups or can_manage_welcome_emails or can_manage_builders or can_view_docs or can_access_django_admin_link %}
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<h2>{% trans "Admin Apps" %}</h2>
|
<h2>{% trans "Admin Apps" %}</h2>
|
||||||
<p>{% trans "Konfiguration, Tests und Steuerung." %}</p>
|
<p>{% trans "Konfiguration, Tests und Steuerung." %}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-grid">
|
<div class="admin-grid">
|
||||||
|
{% if can_manage_integrations %}
|
||||||
<section class="admin-card">
|
<section class="admin-card">
|
||||||
<h3>{% trans "Integrationen" %}</h3>
|
<h3>{% trans "Integrationen" %}</h3>
|
||||||
<p>{% trans "Nextcloud- und E-Mail-Setup." %}</p>
|
<p>{% trans "Nextcloud- und E-Mail-Setup." %}</p>
|
||||||
<a class="btn btn-secondary" href="/admin-tools/integrations/?kind=nextcloud">{% trans "Öffnen" %}</a>
|
<a class="btn btn-secondary" href="/admin-tools/integrations/?kind=nextcloud">{% trans "Öffnen" %}</a>
|
||||||
</section>
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% if can_manage_users %}
|
||||||
|
<section class="admin-card">
|
||||||
|
<h3>{% trans "Benutzer & Rollen" %}</h3>
|
||||||
|
<p>{% trans "Benutzer anlegen, Rollen zuweisen und Zugriffe steuern." %}</p>
|
||||||
|
<a class="btn btn-secondary" href="/admin-tools/users/">{% trans "Öffnen" %}</a>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% if can_view_audit_log %}
|
||||||
<section class="admin-card">
|
<section class="admin-card">
|
||||||
<h3>{% trans "Audit Log" %}</h3>
|
<h3>{% trans "Audit Log" %}</h3>
|
||||||
<p>{% trans "Wichtige Admin-Aktionen nachvollziehen und prüfen." %}</p>
|
<p>{% trans "Wichtige Admin-Aktionen nachvollziehen und prüfen." %}</p>
|
||||||
<a class="btn btn-secondary" href="/admin-tools/audit-log/">{% trans "Öffnen" %}</a>
|
<a class="btn btn-secondary" href="/admin-tools/audit-log/">{% trans "Öffnen" %}</a>
|
||||||
</section>
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% if can_manage_backups %}
|
||||||
<section class="admin-card">
|
<section class="admin-card">
|
||||||
<h3>{% trans "Backup & Recovery" %}</h3>
|
<h3>{% trans "Backup & Recovery" %}</h3>
|
||||||
<p>{% trans "Backups erstellen und sicher verifizieren." %}</p>
|
<p>{% trans "Backups erstellen und sicher verifizieren." %}</p>
|
||||||
<a class="btn btn-secondary" href="/admin-tools/backups/">{% trans "Öffnen" %}</a>
|
<a class="btn btn-secondary" href="/admin-tools/backups/">{% trans "Öffnen" %}</a>
|
||||||
</section>
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% if can_manage_welcome_emails %}
|
||||||
<section class="admin-card">
|
<section class="admin-card">
|
||||||
<h3>{% trans "Welcome E-Mails" %}</h3>
|
<h3>{% trans "Welcome E-Mails" %}</h3>
|
||||||
<p>{% trans "Geplante Welcome Mails verwalten." %}</p>
|
<p>{% trans "Geplante Welcome Mails verwalten." %}</p>
|
||||||
<a class="btn btn-secondary" href="/admin-tools/welcome-emails/">{% trans "Öffnen" %}</a>
|
<a class="btn btn-secondary" href="/admin-tools/welcome-emails/">{% trans "Öffnen" %}</a>
|
||||||
</section>
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% if can_manage_builders %}
|
||||||
<section class="admin-card">
|
<section class="admin-card">
|
||||||
<h3>{% trans "Form Builder" %}</h3>
|
<h3>{% trans "Form Builder" %}</h3>
|
||||||
<p>{% trans "Felder, Schritte und Optionen verwalten." %}</p>
|
<p>{% trans "Felder, Schritte und Optionen verwalten." %}</p>
|
||||||
@@ -141,16 +159,21 @@
|
|||||||
<p>{% trans "Checklistenpunkte für das Einweisungsprotokoll konfigurieren." %}</p>
|
<p>{% trans "Checklistenpunkte für das Einweisungsprotokoll konfigurieren." %}</p>
|
||||||
<a class="btn btn-secondary" href="/admin-tools/intro-builder/">{% trans "Öffnen" %}</a>
|
<a class="btn btn-secondary" href="/admin-tools/intro-builder/">{% trans "Öffnen" %}</a>
|
||||||
</section>
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% if can_view_docs %}
|
||||||
<section class="admin-card">
|
<section class="admin-card">
|
||||||
<h3>{% trans "Handbook" %}</h3>
|
<h3>{% trans "Handbook" %}</h3>
|
||||||
<p>{% trans "Project wiki and developer documentation in one place." %}</p>
|
<p>{% trans "Project wiki and developer documentation in one place." %}</p>
|
||||||
<a class="btn btn-secondary" href="/admin-tools/handbook/">{% trans "Öffnen" %}</a>
|
<a class="btn btn-secondary" href="/admin-tools/handbook/">{% trans "Öffnen" %}</a>
|
||||||
</section>
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% if can_access_django_admin_link %}
|
||||||
<section class="admin-card">
|
<section class="admin-card">
|
||||||
<h3>{% trans "Django Admin" %}</h3>
|
<h3>{% trans "Django Admin" %}</h3>
|
||||||
<p>{% trans "Vollständige Datenverwaltung." %}</p>
|
<p>{% trans "Vollständige Datenverwaltung." %}</p>
|
||||||
<a class="btn btn-secondary" href="/admin/">{% trans "Öffnen" %}</a>
|
<a class="btn btn-secondary" href="/admin/">{% trans "Öffnen" %}</a>
|
||||||
</section>
|
</section>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -172,9 +172,13 @@
|
|||||||
|
|
||||||
<h2 id="admin">9) Admin Apps (Home)</h2>
|
<h2 id="admin">9) Admin Apps (Home)</h2>
|
||||||
<ul>
|
<ul>
|
||||||
|
<li><strong>Rollenmodell:</strong> the app now uses four named roles: <code>Super Admin</code>, <code>Admin</code>, <code>IT Staff</code>, and <code>Staff</code>.</li>
|
||||||
|
<li><strong>Zugriffslogik:</strong> page visibility and critical actions are controlled by capability checks, not only by <code>is_staff</code>.</li>
|
||||||
|
<li><strong>Fallback-Verhalten:</strong> legacy staff users without an explicit role group currently fall back to <code>Admin</code> access so existing operations do not break during rollout.</li>
|
||||||
<li><strong>Form Builder:</strong> manage field visibility/order/options.</li>
|
<li><strong>Form Builder:</strong> manage field visibility/order/options.</li>
|
||||||
<li><strong>Einweisungs-Builder:</strong> manage custom checklist items for the intro PDF and live introduction checklist, including section, visibility, and conditional display logic.</li>
|
<li><strong>Einweisungs-Builder:</strong> manage custom checklist items for the intro PDF and live introduction checklist, including section, visibility, and conditional display logic.</li>
|
||||||
<li><strong>Integrations:</strong> Nextcloud, SMTP, default routing addresses, notification rules, workflow rules, and remote backup target settings.</li>
|
<li><strong>Integrations:</strong> Nextcloud, SMTP, default routing addresses, notification rules, workflow rules, and remote backup target settings.</li>
|
||||||
|
<li><strong>Benutzer & Rollen:</strong> super-admin-only page for creating users, assigning roles, activating/deactivating access, sending password-reset links, and deleting accounts when appropriate.</li>
|
||||||
<li><strong>Welcome Emails:</strong> scheduled jobs, pause/resume/cancel/trigger now.</li>
|
<li><strong>Welcome Emails:</strong> scheduled jobs, pause/resume/cancel/trigger now.</li>
|
||||||
<li><strong>Audit Log:</strong> staff-only trace of important admin changes such as builder edits, settings updates, PDF generation, welcome-email operations, and request deletions. Supports filtering by action, user, and date range.</li>
|
<li><strong>Audit Log:</strong> staff-only trace of important admin changes such as builder edits, settings updates, PDF generation, welcome-email operations, and request deletions. Supports filtering by action, user, and date range.</li>
|
||||||
<li><strong>Requests Dashboard:</strong> search records, open PDFs, delete records (single/bulk for staff).</li>
|
<li><strong>Requests Dashboard:</strong> search records, open PDFs, delete records (single/bulk for staff).</li>
|
||||||
@@ -223,6 +227,7 @@
|
|||||||
|
|
||||||
<h2 id="hardening">11) Security & Reliability Hardening (Current)</h2>
|
<h2 id="hardening">11) Security & Reliability Hardening (Current)</h2>
|
||||||
<ul>
|
<ul>
|
||||||
|
<li><strong>Rollen & Berechtigungen:</strong> operational areas such as Integrationen, Builders, Audit Log, Backup & Recovery, request delete/retry, and intro-session actions are now protected by capability-based role checks.</li>
|
||||||
<li><strong>Cookie + header hardening:</strong> HTTPOnly cookies, SameSite cookies, <code>X-Content-Type-Options: nosniff</code>, stricter referrer policy, and frame protection.</li>
|
<li><strong>Cookie + header hardening:</strong> HTTPOnly cookies, SameSite cookies, <code>X-Content-Type-Options: nosniff</code>, stricter referrer policy, and frame protection.</li>
|
||||||
<li><strong>Optional secure-cookie mode:</strong> can be enabled via environment for HTTPS deployments.</li>
|
<li><strong>Optional secure-cookie mode:</strong> can be enabled via environment for HTTPS deployments.</li>
|
||||||
<li><strong>Upload guards:</strong> server-side upload size limits plus signature image magic-byte validation for PNG/JPEG.</li>
|
<li><strong>Upload guards:</strong> server-side upload size limits plus signature image magic-byte validation for PNG/JPEG.</li>
|
||||||
|
|||||||
@@ -167,7 +167,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% if request.user.is_staff %}
|
{% if can_delete_requests %}
|
||||||
<div class="control-stack">
|
<div class="control-stack">
|
||||||
<form method="post" action="/requests/" id="bulk-delete-form" data-confirm="{% trans 'Ausgewählte Einträge wirklich löschen?' %}">
|
<form method="post" action="/requests/" id="bulk-delete-form" data-confirm="{% trans 'Ausgewählte Einträge wirklich löschen?' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@@ -184,19 +184,19 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{% if request.user.is_staff %}<th class="select-col"><input type="checkbox" id="select-all" aria-label="Alle auswählen" /></th>{% endif %}
|
{% if can_delete_requests %}<th class="select-col"><input type="checkbox" id="select-all" aria-label="Alle auswählen" /></th>{% endif %}
|
||||||
<th>{% trans "Typ" %}</th>
|
<th>{% trans "Typ" %}</th>
|
||||||
<th>{% trans "Person" %}</th>
|
<th>{% trans "Person" %}</th>
|
||||||
<th>{% trans "E-Mail" %}</th>
|
<th>{% trans "E-Mail" %}</th>
|
||||||
<th>{% trans "Dokument" %}</th>
|
<th>{% trans "Dokument" %}</th>
|
||||||
{% if request.user.is_staff %}<th>{% trans "Einweisung" %}</th>{% endif %}
|
{% if can_run_intro_session or can_generate_intro_pdfs %}<th>{% trans "Einweisung" %}</th>{% endif %}
|
||||||
{% if request.user.is_staff %}<th>{% trans "Aktion" %}</th>{% endif %}
|
{% if can_retry_requests or can_delete_requests or can_access_requests_dashboard %}<th>{% trans "Aktion" %}</th>{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in rows %}
|
{% for row in rows %}
|
||||||
<tr>
|
<tr>
|
||||||
{% if request.user.is_staff %}
|
{% if can_delete_requests %}
|
||||||
<td class="select-col">
|
<td class="select-col">
|
||||||
<input type="checkbox" class="row-select" name="selected_requests" value="{{ row.kind_slug }}:{{ row.id }}" aria-label="{{ row.kind }} {{ row.name }}" form="bulk-delete-form" />
|
<input type="checkbox" class="row-select" name="selected_requests" value="{{ row.kind_slug }}:{{ row.id }}" aria-label="{{ row.kind }} {{ row.name }}" form="bulk-delete-form" />
|
||||||
</td>
|
</td>
|
||||||
@@ -225,7 +225,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% if request.user.is_staff %}
|
{% if can_run_intro_session or can_generate_intro_pdfs %}
|
||||||
<td class="actions-cell intro-panel">
|
<td class="actions-cell intro-panel">
|
||||||
{% if row.kind_slug == 'onboarding' %}
|
{% if row.kind_slug == 'onboarding' %}
|
||||||
<details>
|
<details>
|
||||||
@@ -234,12 +234,15 @@
|
|||||||
<div class="intro-group">
|
<div class="intro-group">
|
||||||
<div class="intro-group-title">{% trans "Live-Protokoll" %}</div>
|
<div class="intro-group-title">{% trans "Live-Protokoll" %}</div>
|
||||||
<div class="intro-actions">
|
<div class="intro-actions">
|
||||||
|
{% if can_run_intro_session %}
|
||||||
<a class="btn btn-secondary" href="/requests/onboarding/{{ row.id }}/intro-session/">{% trans "Einweisung öffnen" %}</a>
|
<a class="btn btn-secondary" href="/requests/onboarding/{{ row.id }}/intro-session/">{% trans "Einweisung öffnen" %}</a>
|
||||||
{% if row.intro_session and row.intro_session.exported_pdf_url %}
|
{% endif %}
|
||||||
|
{% if can_run_intro_session and row.intro_session and row.intro_session.exported_pdf_url %}
|
||||||
<a class="btn btn-secondary" href="{{ row.intro_session.exported_pdf_url }}" target="_blank" rel="noopener">{% trans "Live-Protokoll öffnen" %}</a>
|
<a class="btn btn-secondary" href="{{ row.intro_session.exported_pdf_url }}" target="_blank" rel="noopener">{% trans "Live-Protokoll öffnen" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if can_generate_intro_pdfs %}
|
||||||
<div class="intro-group">
|
<div class="intro-group">
|
||||||
<div class="intro-group-title">{% trans "Standard-Einweisungs-PDF" %}</div>
|
<div class="intro-group-title">{% trans "Standard-Einweisungs-PDF" %}</div>
|
||||||
<div class="intro-actions">
|
<div class="intro-actions">
|
||||||
@@ -257,6 +260,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if row.intro_session %}
|
{% if row.intro_session %}
|
||||||
<div class="intro-meta">{% trans "Status:" %} {{ row.intro_session.get_status_display }}</div>
|
<div class="intro-meta">{% trans "Status:" %} {{ row.intro_session.get_status_display }}</div>
|
||||||
@@ -266,24 +270,28 @@
|
|||||||
<span class="person-meta">{% trans "Nicht relevant" %}</span>
|
<span class="person-meta">{% trans "Nicht relevant" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
{% if can_retry_requests or can_delete_requests or can_access_requests_dashboard %}
|
||||||
<td class="actions-cell">
|
<td class="actions-cell">
|
||||||
<a class="btn btn-secondary" href="/requests/timeline/{{ row.kind_slug }}/{{ row.id }}/">{% trans "Timeline" %}</a>
|
<a class="btn btn-secondary" href="/requests/timeline/{{ row.kind_slug }}/{{ row.id }}/">{% trans "Timeline" %}</a>
|
||||||
{% if row.status_key == 'failed' %}
|
{% if can_retry_requests and row.status_key == 'failed' %}
|
||||||
<form method="post" action="/requests/retry/{{ row.kind_slug }}/{{ row.id }}/" class="inline-delete" data-confirm="{% trans 'Eintrag erneut verarbeiten?' %}">
|
<form method="post" action="/requests/retry/{{ row.kind_slug }}/{{ row.id }}/" class="inline-delete" data-confirm="{% trans 'Eintrag erneut verarbeiten?' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="btn btn-secondary" type="submit">{% trans "Erneut versuchen" %}</button>
|
<button class="btn btn-secondary" type="submit">{% trans "Erneut versuchen" %}</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if can_delete_requests %}
|
||||||
<form method="post" action="/requests/" class="inline-delete" data-confirm="{% trans 'Eintrag wirklich löschen?' %}">
|
<form method="post" action="/requests/" class="inline-delete" data-confirm="{% trans 'Eintrag wirklich löschen?' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="btn btn-secondary" type="submit" name="single_delete" value="{{ row.kind_slug }}:{{ row.id }}">{% trans "Löschen" %}</button>
|
<button class="btn btn-secondary" type="submit" name="single_delete" value="{{ row.kind_slug }}:{{ row.id }}">{% trans "Löschen" %}</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{% if request.user.is_staff %}8{% else %}5{% endif %}" class="empty-state">{% trans "Noch keine Vorgänge vorhanden." %}</td>
|
<td colspan="{{ column_count }}" class="empty-state">{% trans "Noch keine Vorgänge vorhanden." %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
151
backend/workflows/templates/workflows/user_management.html
Normal file
151
backend/workflows/templates/workflows/user_management.html
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
{% extends 'workflows/base_shell.html' %}
|
||||||
|
{% load static i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Benutzer & Rollen" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<link rel="stylesheet" href="{% static 'workflows/css/admin_tools.css' %}" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block shell_body %}
|
||||||
|
{% include 'workflows/includes/app_header.html' with header_show_lang=1 header_show_home=1 header_inside_shell=1 %}
|
||||||
|
<div class="toolbar">
|
||||||
|
<div>
|
||||||
|
<h1>{% trans "Benutzer & Rollen" %}</h1>
|
||||||
|
<p class="sub">{% trans "Super Admins verwalten Benutzerkonten, Rollen und den aktiven Zugriff." %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include 'workflows/includes/messages.html' %}
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>{% trans "Benutzer anlegen" %}</h2>
|
||||||
|
<form method="post" action="{% url 'create_user_from_admin' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="grid">
|
||||||
|
<div>
|
||||||
|
<label for="{{ create_form.first_name.id_for_label }}">{{ create_form.first_name.label }}</label>
|
||||||
|
{{ create_form.first_name }}
|
||||||
|
{% for error in create_form.first_name.errors %}<div class="hint">{{ error }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="{{ create_form.last_name.id_for_label }}">{{ create_form.last_name.label }}</label>
|
||||||
|
{{ create_form.last_name }}
|
||||||
|
{% for error in create_form.last_name.errors %}<div class="hint">{{ error }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="{{ create_form.username.id_for_label }}">{{ create_form.username.label }}</label>
|
||||||
|
{{ create_form.username }}
|
||||||
|
{% for error in create_form.username.errors %}<div class="hint">{{ error }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="{{ create_form.email.id_for_label }}">{{ create_form.email.label }}</label>
|
||||||
|
{{ create_form.email }}
|
||||||
|
{% for error in create_form.email.errors %}<div class="hint">{{ error }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="{{ create_form.role_key.id_for_label }}">{{ create_form.role_key.label }}</label>
|
||||||
|
{{ create_form.role_key }}
|
||||||
|
{% for error in create_form.role_key.errors %}<div class="hint">{{ error }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="{{ create_form.password1.id_for_label }}">{{ create_form.password1.label }}</label>
|
||||||
|
{{ create_form.password1 }}
|
||||||
|
{% for error in create_form.password1.errors %}<div class="hint">{{ error }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="{{ create_form.password2.id_for_label }}">{{ create_form.password2.label }}</label>
|
||||||
|
{{ create_form.password2 }}
|
||||||
|
{% for error in create_form.password2.errors %}<div class="hint">{{ error }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% for error in create_form.non_field_errors %}<div class="hint">{{ error }}</div>{% endfor %}
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn btn-primary" type="submit">{% trans "Benutzer erstellen" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<div class="toolbar">
|
||||||
|
<div>
|
||||||
|
<h2>{% trans "Benutzerübersicht" %}</h2>
|
||||||
|
<p class="sub">{% trans "Rollen ändern, Zugriffe sperren oder ein neues Passwort setzen." %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Name" %}</th>
|
||||||
|
<th>{% trans "Benutzername" %}</th>
|
||||||
|
<th>{% trans "E-Mail" %}</th>
|
||||||
|
<th>{% trans "Rolle" %}</th>
|
||||||
|
<th>{% trans "Aktiv" %}</th>
|
||||||
|
<th>{% trans "Letzte Anmeldung" %}</th>
|
||||||
|
<th>{% trans "Neues Passwort" %}</th>
|
||||||
|
<th>{% trans "Aktionen" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in rows %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>{{ row.display_name|default:row.user.username }}</strong>
|
||||||
|
{% if row.user == request.user %}
|
||||||
|
<div class="mini">{% trans "Sie selbst" %}</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ row.user.username }}</td>
|
||||||
|
<td>{{ row.user.email|default:"-" }}</td>
|
||||||
|
<td>
|
||||||
|
<label class="sr-only" for="role_key_{{ row.user.id }}">{% trans "Rolle" %}</label>
|
||||||
|
<select id="role_key_{{ row.user.id }}" name="role_key" form="user-form-{{ row.user.id }}">
|
||||||
|
{% for role_key, role_label in role_choices %}
|
||||||
|
<option value="{{ role_key }}"{% if role_key == row.role_key %} selected{% endif %}>{{ role_label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label class="check-row" for="is_active_{{ row.user.id }}">
|
||||||
|
<input id="is_active_{{ row.user.id }}" type="checkbox" name="is_active" form="user-form-{{ row.user.id }}"{% if row.user.is_active %} checked{% endif %} />
|
||||||
|
<span>{% if row.user.is_active %}{% trans "aktiv" %}{% else %}{% trans "inaktiv" %}{% endif %}</span>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
<td>{{ row.user.last_login|date:"d.m.Y H:i"|default:"-" }}</td>
|
||||||
|
<td>
|
||||||
|
<label class="sr-only" for="new_password_{{ row.user.id }}">{% trans "Neues Passwort" %}</label>
|
||||||
|
<input id="new_password_{{ row.user.id }}" type="password" name="new_password" form="user-form-{{ row.user.id }}" placeholder="{% trans 'Optional' %}" />
|
||||||
|
</td>
|
||||||
|
<td class="actions">
|
||||||
|
<form id="user-form-{{ row.user.id }}" method="post" action="{% url 'update_user_from_admin' row.user.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
</form>
|
||||||
|
<button class="btn btn-secondary" type="submit" form="user-form-{{ row.user.id }}">{% trans "Speichern" %}</button>
|
||||||
|
{% if row.user.email %}
|
||||||
|
<form method="post" action="{% url 'send_password_reset_from_admin' row.user.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="btn btn-secondary" type="submit">{% trans "Reset-Link senden" %}</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<span class="mini">{% trans "Keine E-Mail" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if row.user != request.user %}
|
||||||
|
<form method="post" action="{% url 'delete_user_from_admin' row.user.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="btn btn-secondary" type="submit" data-confirm="1" data-confirm-message="{% trans 'Benutzer wirklich löschen?' %}">{% trans "Löschen" %}</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="8">{% trans "Es sind noch keine Benutzer vorhanden." %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p class="hint">{% trans "Hinweis: Der aktuell angemeldete Super Admin kann sich hier nicht selbst deaktivieren oder auf eine niedrigere Rolle setzen." %}</p>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@@ -30,6 +30,11 @@ urlpatterns = [
|
|||||||
path('admin-tools/welcome-emails/<int:schedule_id>/resume/', views.resume_welcome_email, name='resume_welcome_email'),
|
path('admin-tools/welcome-emails/<int:schedule_id>/resume/', views.resume_welcome_email, name='resume_welcome_email'),
|
||||||
path('admin-tools/welcome-emails/<int:schedule_id>/cancel/', views.cancel_welcome_email, name='cancel_welcome_email'),
|
path('admin-tools/welcome-emails/<int:schedule_id>/cancel/', views.cancel_welcome_email, name='cancel_welcome_email'),
|
||||||
path('admin-tools/handbook/', views.handbook_page, name='handbook_page'),
|
path('admin-tools/handbook/', views.handbook_page, name='handbook_page'),
|
||||||
|
path('admin-tools/users/', views.user_management_page, name='user_management_page'),
|
||||||
|
path('admin-tools/users/create/', views.create_user_from_admin, name='create_user_from_admin'),
|
||||||
|
path('admin-tools/users/<int:user_id>/update/', views.update_user_from_admin, name='update_user_from_admin'),
|
||||||
|
path('admin-tools/users/<int:user_id>/send-password-reset/', views.send_password_reset_from_admin, name='send_password_reset_from_admin'),
|
||||||
|
path('admin-tools/users/<int:user_id>/delete/', views.delete_user_from_admin, name='delete_user_from_admin'),
|
||||||
path('admin-tools/wiki/', views.project_wiki_page, name='project_wiki_page'),
|
path('admin-tools/wiki/', views.project_wiki_page, name='project_wiki_page'),
|
||||||
path('admin-tools/developer-handbook/', views.developer_handbook_page, name='developer_handbook_page'),
|
path('admin-tools/developer-handbook/', views.developer_handbook_page, name='developer_handbook_page'),
|
||||||
path('admin-tools/release-checklist/', views.release_checklist_page, name='release_checklist_page'),
|
path('admin-tools/release-checklist/', views.release_checklist_page, name='release_checklist_page'),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from pathlib import Path
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
import json
|
import json
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
from celery import current_app
|
from celery import current_app
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -10,16 +11,21 @@ from django.db import IntegrityError
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib.auth.tokens import default_token_generator
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.encoding import force_bytes
|
||||||
|
from django.utils.http import urlsafe_base64_encode
|
||||||
from django.utils.translation import gettext as _, gettext_lazy
|
from django.utils.translation import gettext as _, gettext_lazy
|
||||||
from django.utils.translation import get_language, override
|
from django.utils.translation import get_language, override
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from .backup_ops import create_backup_bundle, delete_backup_bundle, list_backup_bundles, verify_backup_bundle
|
from .backup_ops import create_backup_bundle, delete_backup_bundle, list_backup_bundles, verify_backup_bundle
|
||||||
from .forms import OffboardingRequestForm, OnboardingRequestForm
|
from .forms import OffboardingRequestForm, OnboardingRequestForm, UserManagementCreateForm
|
||||||
from .form_builder import (
|
from .form_builder import (
|
||||||
DEFAULT_FIELD_ORDER,
|
DEFAULT_FIELD_ORDER,
|
||||||
LOCKED_FIELD_RULES,
|
LOCKED_FIELD_RULES,
|
||||||
@@ -30,6 +36,7 @@ from .form_builder import (
|
|||||||
)
|
)
|
||||||
from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig
|
from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig
|
||||||
from .emailing import send_system_email
|
from .emailing import send_system_email
|
||||||
|
from .roles import ROLE_GROUP_NAMES, ROLE_LABELS, ROLE_SUPER_ADMIN, assign_user_role, get_user_role_key, get_user_role_label, user_has_capability
|
||||||
from .services import get_email_test_redirect, is_email_test_mode, is_nextcloud_enabled, upload_to_nextcloud
|
from .services import get_email_test_redirect, is_email_test_mode, is_nextcloud_enabled, upload_to_nextcloud
|
||||||
from .tasks import (
|
from .tasks import (
|
||||||
DEFAULT_NOTIFICATION_TEMPLATES,
|
DEFAULT_NOTIFICATION_TEMPLATES,
|
||||||
@@ -112,8 +119,19 @@ def healthz(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _is_staff(user) -> bool:
|
def _require_capability(capability: str):
|
||||||
return user.is_authenticated and user.is_staff
|
def decorator(view_func):
|
||||||
|
@wraps(view_func)
|
||||||
|
@login_required
|
||||||
|
def wrapped(request, *args, **kwargs):
|
||||||
|
if not user_has_capability(request.user, capability):
|
||||||
|
messages.error(request, _('Sie haben keine Berechtigung für diese Aktion.'))
|
||||||
|
return redirect('home')
|
||||||
|
return view_func(request, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def _display_user_name(user) -> str:
|
def _display_user_name(user) -> str:
|
||||||
@@ -218,6 +236,10 @@ def _audit_action_label(action: str) -> str:
|
|||||||
'mail_settings_saved': _('Mail-Einstellungen gespeichert'),
|
'mail_settings_saved': _('Mail-Einstellungen gespeichert'),
|
||||||
'email_routing_saved': _('E-Mail-Routing gespeichert'),
|
'email_routing_saved': _('E-Mail-Routing gespeichert'),
|
||||||
'notification_rules_saved': _('Benachrichtigungsregeln gespeichert'),
|
'notification_rules_saved': _('Benachrichtigungsregeln gespeichert'),
|
||||||
|
'user_created': _('Benutzer erstellt'),
|
||||||
|
'user_updated': _('Benutzer aktualisiert'),
|
||||||
|
'user_password_reset_sent': _('Passwort-Reset-Link versendet'),
|
||||||
|
'user_deleted': _('Benutzer gelöscht'),
|
||||||
'backup_created': _('Backup erstellt'),
|
'backup_created': _('Backup erstellt'),
|
||||||
'backup_verified': _('Backup verifiziert'),
|
'backup_verified': _('Backup verifiziert'),
|
||||||
'backup_deleted': _('Backup gelöscht'),
|
'backup_deleted': _('Backup gelöscht'),
|
||||||
@@ -311,36 +333,220 @@ def home(request):
|
|||||||
'nextcloud_enabled': is_nextcloud_enabled(),
|
'nextcloud_enabled': is_nextcloud_enabled(),
|
||||||
'email_test_mode': is_email_test_mode(),
|
'email_test_mode': is_email_test_mode(),
|
||||||
'workflow_config': config,
|
'workflow_config': config,
|
||||||
|
'role_label': get_user_role_label(request.user),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
def _user_management_rows():
|
||||||
@user_passes_test(_is_staff)
|
user_model = get_user_model()
|
||||||
|
role_order = {
|
||||||
|
ROLE_SUPER_ADMIN: 0,
|
||||||
|
'admin': 1,
|
||||||
|
'it_staff': 2,
|
||||||
|
'staff': 3,
|
||||||
|
}
|
||||||
|
rows = []
|
||||||
|
for user in user_model.objects.all().order_by('-is_active', 'username'):
|
||||||
|
role_key = get_user_role_key(user)
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
'user': user,
|
||||||
|
'role_key': role_key,
|
||||||
|
'role_label': str(ROLE_LABELS[role_key]),
|
||||||
|
'role_sort': role_order.get(role_key, 99),
|
||||||
|
'display_name': _display_user_name(user),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
rows.sort(key=lambda item: (not item['user'].is_active, item['role_sort'], item['user'].username.lower()))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _render_user_management(request, create_form=None, status_code: int = 200):
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
'workflows/user_management.html',
|
||||||
|
{
|
||||||
|
'create_form': create_form or UserManagementCreateForm(),
|
||||||
|
'rows': _user_management_rows(),
|
||||||
|
'role_choices': [(key, str(ROLE_LABELS[key])) for key in ROLE_GROUP_NAMES],
|
||||||
|
},
|
||||||
|
status=status_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _super_admin_user_count() -> int:
|
||||||
|
user_model = get_user_model()
|
||||||
|
return sum(1 for user in user_model.objects.all() if get_user_role_key(user) == ROLE_SUPER_ADMIN and user.is_active)
|
||||||
|
|
||||||
|
|
||||||
|
def _would_remove_last_super_admin(user, new_role_key: str | None = None, new_is_active: bool | None = None, deleting: bool = False) -> bool:
|
||||||
|
if get_user_role_key(user) != ROLE_SUPER_ADMIN or not user.is_active:
|
||||||
|
return False
|
||||||
|
if _super_admin_user_count() > 1:
|
||||||
|
return False
|
||||||
|
if deleting:
|
||||||
|
return True
|
||||||
|
if new_role_key is not None and new_role_key != ROLE_SUPER_ADMIN:
|
||||||
|
return True
|
||||||
|
if new_is_active is not None and not new_is_active:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@_require_capability('manage_users')
|
||||||
|
def user_management_page(request):
|
||||||
|
return _render_user_management(request)
|
||||||
|
|
||||||
|
|
||||||
|
@_require_capability('manage_users')
|
||||||
|
@require_POST
|
||||||
|
def create_user_from_admin(request):
|
||||||
|
form = UserManagementCreateForm(request.POST)
|
||||||
|
if not form.is_valid():
|
||||||
|
messages.error(request, _('Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben.'))
|
||||||
|
return _render_user_management(request, create_form=form, status_code=400)
|
||||||
|
|
||||||
|
user = form.save()
|
||||||
|
_audit(
|
||||||
|
request,
|
||||||
|
'user_created',
|
||||||
|
target_type='user',
|
||||||
|
target_id=user.id,
|
||||||
|
target_label=_display_user_name(user),
|
||||||
|
details={'username': user.username, 'role': get_user_role_key(user)},
|
||||||
|
)
|
||||||
|
messages.success(request, _('Benutzer wurde erstellt: %(username)s') % {'username': user.username})
|
||||||
|
return redirect('user_management_page')
|
||||||
|
|
||||||
|
|
||||||
|
@_require_capability('manage_users')
|
||||||
|
@require_POST
|
||||||
|
def update_user_from_admin(request, user_id: int):
|
||||||
|
user_model = get_user_model()
|
||||||
|
target_user = get_object_or_404(user_model, id=user_id)
|
||||||
|
role_key = (request.POST.get('role_key') or '').strip()
|
||||||
|
is_active = request.POST.get('is_active') == 'on'
|
||||||
|
new_password = (request.POST.get('new_password') or '').strip()
|
||||||
|
|
||||||
|
if role_key not in ROLE_GROUP_NAMES:
|
||||||
|
messages.error(request, _('Ungültige Rolle.'))
|
||||||
|
return redirect('user_management_page')
|
||||||
|
|
||||||
|
if target_user == request.user and (role_key != ROLE_SUPER_ADMIN or not is_active):
|
||||||
|
messages.error(request, _('Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren oder herabstufen.'))
|
||||||
|
return redirect('user_management_page')
|
||||||
|
if _would_remove_last_super_admin(target_user, new_role_key=role_key, new_is_active=is_active):
|
||||||
|
messages.error(request, _('Der letzte aktive Super Admin kann nicht deaktiviert oder herabgestuft werden.'))
|
||||||
|
return redirect('user_management_page')
|
||||||
|
|
||||||
|
assign_user_role(target_user, role_key)
|
||||||
|
target_user.is_active = is_active
|
||||||
|
if new_password:
|
||||||
|
target_user.set_password(new_password)
|
||||||
|
target_user.save()
|
||||||
|
|
||||||
|
_audit(
|
||||||
|
request,
|
||||||
|
'user_updated',
|
||||||
|
target_type='user',
|
||||||
|
target_id=target_user.id,
|
||||||
|
target_label=_display_user_name(target_user),
|
||||||
|
details={'username': target_user.username, 'role': role_key, 'is_active': is_active, 'password_changed': bool(new_password)},
|
||||||
|
)
|
||||||
|
messages.success(request, _('Benutzer wurde aktualisiert: %(username)s') % {'username': target_user.username})
|
||||||
|
return redirect('user_management_page')
|
||||||
|
|
||||||
|
|
||||||
|
@_require_capability('manage_users')
|
||||||
|
@require_POST
|
||||||
|
def send_password_reset_from_admin(request, user_id: int):
|
||||||
|
user_model = get_user_model()
|
||||||
|
target_user = get_object_or_404(user_model, id=user_id)
|
||||||
|
email = (target_user.email or '').strip()
|
||||||
|
if not email:
|
||||||
|
messages.error(request, _('Für diesen Benutzer ist keine E-Mail-Adresse hinterlegt.'))
|
||||||
|
return redirect('user_management_page')
|
||||||
|
|
||||||
|
uid = urlsafe_base64_encode(force_bytes(target_user.pk))
|
||||||
|
token = default_token_generator.make_token(target_user)
|
||||||
|
reset_path = reverse('password_reset_confirm', kwargs={'uidb64': uid, 'token': token})
|
||||||
|
reset_url = request.build_absolute_uri(reset_path)
|
||||||
|
|
||||||
|
send_system_email(
|
||||||
|
subject=_('Passwort zurücksetzen für %(username)s') % {'username': target_user.username},
|
||||||
|
body=_(
|
||||||
|
'Hallo %(name)s,\n\n'
|
||||||
|
'für Ihr Konto wurde ein Link zum Zurücksetzen des Passworts erstellt.\n'
|
||||||
|
'Bitte öffnen Sie den folgenden Link:\n'
|
||||||
|
'%(url)s\n\n'
|
||||||
|
'Wenn Sie diese Anfrage nicht erwartet haben, können Sie diese E-Mail ignorieren.'
|
||||||
|
) % {
|
||||||
|
'name': _display_user_name(target_user),
|
||||||
|
'url': reset_url,
|
||||||
|
},
|
||||||
|
to=[email],
|
||||||
|
)
|
||||||
|
_audit(
|
||||||
|
request,
|
||||||
|
'user_password_reset_sent',
|
||||||
|
target_type='user',
|
||||||
|
target_id=target_user.id,
|
||||||
|
target_label=_display_user_name(target_user),
|
||||||
|
details={'username': target_user.username, 'email': email},
|
||||||
|
)
|
||||||
|
messages.success(request, _('Passwort-Reset-Link wurde versendet: %(username)s') % {'username': target_user.username})
|
||||||
|
return redirect('user_management_page')
|
||||||
|
|
||||||
|
|
||||||
|
@_require_capability('manage_users')
|
||||||
|
@require_POST
|
||||||
|
def delete_user_from_admin(request, user_id: int):
|
||||||
|
user_model = get_user_model()
|
||||||
|
target_user = get_object_or_404(user_model, id=user_id)
|
||||||
|
|
||||||
|
if target_user == request.user:
|
||||||
|
messages.error(request, _('Der aktuell angemeldete Super Admin kann sich hier nicht selbst löschen.'))
|
||||||
|
return redirect('user_management_page')
|
||||||
|
if _would_remove_last_super_admin(target_user, deleting=True):
|
||||||
|
messages.error(request, _('Der letzte aktive Super Admin kann nicht gelöscht werden.'))
|
||||||
|
return redirect('user_management_page')
|
||||||
|
|
||||||
|
target_label = _display_user_name(target_user)
|
||||||
|
username = target_user.username
|
||||||
|
target_user.delete()
|
||||||
|
_audit(
|
||||||
|
request,
|
||||||
|
'user_deleted',
|
||||||
|
target_type='user',
|
||||||
|
target_label=target_label,
|
||||||
|
details={'username': username},
|
||||||
|
)
|
||||||
|
messages.success(request, _('Benutzer wurde gelöscht: %(username)s') % {'username': username})
|
||||||
|
return redirect('user_management_page')
|
||||||
|
|
||||||
|
|
||||||
|
@_require_capability('view_docs')
|
||||||
def handbook_page(request):
|
def handbook_page(request):
|
||||||
return render(request, 'workflows/handbook.html')
|
return render(request, 'workflows/handbook.html')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('view_docs')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
def project_wiki_page(request):
|
def project_wiki_page(request):
|
||||||
return render(request, 'workflows/project_wiki.html')
|
return render(request, 'workflows/project_wiki.html')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('view_docs')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
def developer_handbook_page(request):
|
def developer_handbook_page(request):
|
||||||
return render(request, 'workflows/developer_handbook.html')
|
return render(request, 'workflows/developer_handbook.html')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('view_docs')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
def release_checklist_page(request):
|
def release_checklist_page(request):
|
||||||
return render(request, 'workflows/release_checklist.html')
|
return render(request, 'workflows/release_checklist.html')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('view_audit_log')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
def audit_log_page(request):
|
def audit_log_page(request):
|
||||||
action = (request.GET.get('action') or '').strip()
|
action = (request.GET.get('action') or '').strip()
|
||||||
user_query = (request.GET.get('user') or '').strip()
|
user_query = (request.GET.get('user') or '').strip()
|
||||||
@@ -380,8 +586,7 @@ def audit_log_page(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_backups')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
def backup_recovery_page(request):
|
def backup_recovery_page(request):
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
@@ -392,8 +597,7 @@ def backup_recovery_page(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_backups')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def create_backup_from_admin(request):
|
def create_backup_from_admin(request):
|
||||||
try:
|
try:
|
||||||
@@ -411,8 +615,7 @@ def create_backup_from_admin(request):
|
|||||||
return redirect('backup_recovery_page')
|
return redirect('backup_recovery_page')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_backups')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def verify_backup_from_admin(request, backup_name: str):
|
def verify_backup_from_admin(request, backup_name: str):
|
||||||
try:
|
try:
|
||||||
@@ -430,8 +633,7 @@ def verify_backup_from_admin(request, backup_name: str):
|
|||||||
return redirect('backup_recovery_page')
|
return redirect('backup_recovery_page')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_backups')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def delete_backup_from_admin(request, backup_name: str):
|
def delete_backup_from_admin(request, backup_name: str):
|
||||||
try:
|
try:
|
||||||
@@ -449,8 +651,7 @@ def delete_backup_from_admin(request, backup_name: str):
|
|||||||
return redirect('backup_recovery_page')
|
return redirect('backup_recovery_page')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('access_requests_dashboard')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
def request_timeline_page(request, kind: str, request_id: int):
|
def request_timeline_page(request, kind: str, request_id: int):
|
||||||
if kind == 'onboarding':
|
if kind == 'onboarding':
|
||||||
obj = get_object_or_404(OnboardingRequest, id=request_id)
|
obj = get_object_or_404(OnboardingRequest, id=request_id)
|
||||||
@@ -569,8 +770,12 @@ def request_timeline_page(request, kind: str, request_id: int):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def requests_dashboard(request):
|
def requests_dashboard(request):
|
||||||
|
if not user_has_capability(request.user, 'access_requests_dashboard'):
|
||||||
|
messages.error(request, _('Sie haben keine Berechtigung für diese Aktion.'))
|
||||||
|
return redirect('home')
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
if not request.user.is_staff:
|
if not user_has_capability(request.user, 'delete_requests'):
|
||||||
messages.error(request, _('Sie haben keine Berechtigung für diese Aktion.'))
|
messages.error(request, _('Sie haben keine Berechtigung für diese Aktion.'))
|
||||||
return redirect('requests_dashboard')
|
return redirect('requests_dashboard')
|
||||||
|
|
||||||
@@ -765,6 +970,13 @@ def requests_dashboard(request):
|
|||||||
{'value': 'failed', 'label': _request_status_label('failed', language_code)},
|
{'value': 'failed', 'label': _request_status_label('failed', language_code)},
|
||||||
]
|
]
|
||||||
has_filters = any([search_query, type_filter, status_filter, department_filter, date_from, date_to])
|
has_filters = any([search_query, type_filter, status_filter, department_filter, date_from, date_to])
|
||||||
|
column_count = 4
|
||||||
|
if user_has_capability(request.user, 'delete_requests'):
|
||||||
|
column_count += 1
|
||||||
|
if user_has_capability(request.user, 'run_intro_session') or user_has_capability(request.user, 'generate_intro_pdfs'):
|
||||||
|
column_count += 1
|
||||||
|
if user_has_capability(request.user, 'access_requests_dashboard'):
|
||||||
|
column_count += 1
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
'workflows/requests_dashboard.html',
|
'workflows/requests_dashboard.html',
|
||||||
@@ -779,6 +991,7 @@ def requests_dashboard(request):
|
|||||||
'departments': departments,
|
'departments': departments,
|
||||||
'status_choices': status_choices,
|
'status_choices': status_choices,
|
||||||
'has_filters': has_filters,
|
'has_filters': has_filters,
|
||||||
|
'column_count': column_count,
|
||||||
'onboarding_total': onboarding_total,
|
'onboarding_total': onboarding_total,
|
||||||
'offboarding_total': offboarding_total,
|
'offboarding_total': offboarding_total,
|
||||||
'combined_total': onboarding_total + offboarding_total,
|
'combined_total': onboarding_total + offboarding_total,
|
||||||
@@ -838,8 +1051,7 @@ def onboarding_success(request, request_id: int):
|
|||||||
return render(request, 'workflows/onboarding_success.html', {'obj': obj, 'pdf_url': pdf_url})
|
return render(request, 'workflows/onboarding_success.html', {'obj': obj, 'pdf_url': pdf_url})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('generate_intro_pdfs')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def generate_onboarding_intro_pdf(request, request_id: int):
|
def generate_onboarding_intro_pdf(request, request_id: int):
|
||||||
obj = get_object_or_404(OnboardingRequest, id=request_id)
|
obj = get_object_or_404(OnboardingRequest, id=request_id)
|
||||||
@@ -851,8 +1063,7 @@ def generate_onboarding_intro_pdf(request, request_id: int):
|
|||||||
return redirect('requests_dashboard')
|
return redirect('requests_dashboard')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('generate_intro_pdfs')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def generate_onboarding_intro_session_pdf(request, request_id: int):
|
def generate_onboarding_intro_session_pdf(request, request_id: int):
|
||||||
onboarding = get_object_or_404(OnboardingRequest, id=request_id)
|
onboarding = get_object_or_404(OnboardingRequest, id=request_id)
|
||||||
@@ -869,8 +1080,7 @@ def generate_onboarding_intro_session_pdf(request, request_id: int):
|
|||||||
return redirect('onboarding_intro_session_page', request_id=request_id)
|
return redirect('onboarding_intro_session_page', request_id=request_id)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('run_intro_session')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
def onboarding_intro_session_page(request, request_id: int):
|
def onboarding_intro_session_page(request, request_id: int):
|
||||||
onboarding = get_object_or_404(OnboardingRequest, id=request_id)
|
onboarding = get_object_or_404(OnboardingRequest, id=request_id)
|
||||||
session, _created = OnboardingIntroductionSession.objects.get_or_create(onboarding_request=onboarding)
|
session, _created = OnboardingIntroductionSession.objects.get_or_create(onboarding_request=onboarding)
|
||||||
@@ -1024,8 +1234,7 @@ def offboarding_success(request, request_id: int):
|
|||||||
return render(request, 'workflows/offboarding_success.html', {'obj': obj, 'pdf_url': pdf_url})
|
return render(request, 'workflows/offboarding_success.html', {'obj': obj, 'pdf_url': pdf_url})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_builders')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
def form_builder_page(request):
|
def form_builder_page(request):
|
||||||
language_code = get_language()
|
language_code = get_language()
|
||||||
form_type = request.GET.get('form_type', 'onboarding')
|
form_type = request.GET.get('form_type', 'onboarding')
|
||||||
@@ -1200,8 +1409,7 @@ def form_builder_page(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_builders')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
def intro_builder_page(request):
|
def intro_builder_page(request):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
delete_id = (request.POST.get('delete_item_id') or '').strip()
|
delete_id = (request.POST.get('delete_item_id') or '').strip()
|
||||||
@@ -1311,8 +1519,7 @@ def intro_builder_page(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_integrations')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
def integrations_setup_page(request):
|
def integrations_setup_page(request):
|
||||||
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
||||||
kind = (request.GET.get('kind') or 'nextcloud').strip().lower()
|
kind = (request.GET.get('kind') or 'nextcloud').strip().lower()
|
||||||
@@ -1342,8 +1549,7 @@ def integrations_setup_page(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_welcome_emails')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
def welcome_emails_page(request):
|
def welcome_emails_page(request):
|
||||||
rows = ScheduledWelcomeEmail.objects.select_related('onboarding_request').order_by('-send_at', '-id')[:200]
|
rows = ScheduledWelcomeEmail.objects.select_related('onboarding_request').order_by('-send_at', '-id')[:200]
|
||||||
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
||||||
@@ -1373,8 +1579,7 @@ def welcome_emails_page(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_welcome_emails')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def trigger_welcome_email_now(request, schedule_id: int):
|
def trigger_welcome_email_now(request, schedule_id: int):
|
||||||
scheduled = ScheduledWelcomeEmail.objects.filter(id=schedule_id).first()
|
scheduled = ScheduledWelcomeEmail.objects.filter(id=schedule_id).first()
|
||||||
@@ -1395,8 +1600,7 @@ def trigger_welcome_email_now(request, schedule_id: int):
|
|||||||
return redirect('welcome_emails_page')
|
return redirect('welcome_emails_page')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_welcome_emails')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def save_welcome_email_settings(request):
|
def save_welcome_email_settings(request):
|
||||||
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
||||||
@@ -1498,8 +1702,7 @@ def _parse_selected_schedule_ids(raw: str) -> list[int]:
|
|||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_welcome_emails')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def bulk_welcome_email_action(request):
|
def bulk_welcome_email_action(request):
|
||||||
action = (request.POST.get('bulk_action') or '').strip().lower()
|
action = (request.POST.get('bulk_action') or '').strip().lower()
|
||||||
@@ -1569,8 +1772,7 @@ def bulk_welcome_email_action(request):
|
|||||||
return redirect('welcome_emails_page')
|
return redirect('welcome_emails_page')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_welcome_emails')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def pause_welcome_email(request, schedule_id: int):
|
def pause_welcome_email(request, schedule_id: int):
|
||||||
scheduled = ScheduledWelcomeEmail.objects.filter(id=schedule_id).first()
|
scheduled = ScheduledWelcomeEmail.objects.filter(id=schedule_id).first()
|
||||||
@@ -1589,8 +1791,7 @@ def pause_welcome_email(request, schedule_id: int):
|
|||||||
return redirect('welcome_emails_page')
|
return redirect('welcome_emails_page')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_welcome_emails')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def resume_welcome_email(request, schedule_id: int):
|
def resume_welcome_email(request, schedule_id: int):
|
||||||
scheduled = ScheduledWelcomeEmail.objects.filter(id=schedule_id).first()
|
scheduled = ScheduledWelcomeEmail.objects.filter(id=schedule_id).first()
|
||||||
@@ -1612,8 +1813,7 @@ def resume_welcome_email(request, schedule_id: int):
|
|||||||
return redirect('welcome_emails_page')
|
return redirect('welcome_emails_page')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_welcome_emails')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def cancel_welcome_email(request, schedule_id: int):
|
def cancel_welcome_email(request, schedule_id: int):
|
||||||
scheduled = ScheduledWelcomeEmail.objects.filter(id=schedule_id).first()
|
scheduled = ScheduledWelcomeEmail.objects.filter(id=schedule_id).first()
|
||||||
@@ -1633,8 +1833,7 @@ def cancel_welcome_email(request, schedule_id: int):
|
|||||||
return redirect('welcome_emails_page')
|
return redirect('welcome_emails_page')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_builders')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def form_builder_save_order(request):
|
def form_builder_save_order(request):
|
||||||
try:
|
try:
|
||||||
@@ -1698,8 +1897,7 @@ def form_builder_save_order(request):
|
|||||||
return JsonResponse({'ok': True, 'saved_count': len(configs)})
|
return JsonResponse({'ok': True, 'saved_count': len(configs)})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_integrations')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def send_test_email(request):
|
def send_test_email(request):
|
||||||
mode = 'TEST_MODE_ON' if is_email_test_mode() else 'TEST_MODE_OFF'
|
mode = 'TEST_MODE_ON' if is_email_test_mode() else 'TEST_MODE_OFF'
|
||||||
@@ -1718,8 +1916,7 @@ def send_test_email(request):
|
|||||||
return _redirect_back(request, 'home')
|
return _redirect_back(request, 'home')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_integrations')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def nextcloud_test_upload(request):
|
def nextcloud_test_upload(request):
|
||||||
filename = f"nextcloud_test_{timezone.now().strftime('%Y%m%d_%H%M%S')}.txt"
|
filename = f"nextcloud_test_{timezone.now().strftime('%Y%m%d_%H%M%S')}.txt"
|
||||||
@@ -1751,8 +1948,7 @@ def nextcloud_test_upload(request):
|
|||||||
return _redirect_back(request, 'home')
|
return _redirect_back(request, 'home')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_integrations')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def toggle_nextcloud_enabled(request):
|
def toggle_nextcloud_enabled(request):
|
||||||
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
||||||
@@ -1766,8 +1962,7 @@ def toggle_nextcloud_enabled(request):
|
|||||||
return _redirect_back(request, 'home')
|
return _redirect_back(request, 'home')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_integrations')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def toggle_email_mode(request):
|
def toggle_email_mode(request):
|
||||||
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
||||||
@@ -1781,8 +1976,7 @@ def toggle_email_mode(request):
|
|||||||
return _redirect_back(request, 'home')
|
return _redirect_back(request, 'home')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_integrations')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def save_integrations_settings(request):
|
def save_integrations_settings(request):
|
||||||
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
||||||
@@ -1820,8 +2014,7 @@ def save_integrations_settings(request):
|
|||||||
return redirect('home')
|
return redirect('home')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_integrations')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def save_nextcloud_settings(request):
|
def save_nextcloud_settings(request):
|
||||||
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
||||||
@@ -1846,8 +2039,7 @@ def save_nextcloud_settings(request):
|
|||||||
return redirect('/admin-tools/integrations/?kind=nextcloud')
|
return redirect('/admin-tools/integrations/?kind=nextcloud')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_integrations')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def save_workflow_rules(request):
|
def save_workflow_rules(request):
|
||||||
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
||||||
@@ -1877,8 +2069,7 @@ def save_workflow_rules(request):
|
|||||||
return redirect('/admin-tools/integrations/?kind=rules')
|
return redirect('/admin-tools/integrations/?kind=rules')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_integrations')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def save_backup_settings(request):
|
def save_backup_settings(request):
|
||||||
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
||||||
@@ -1930,8 +2121,7 @@ def save_backup_settings(request):
|
|||||||
return redirect('/admin-tools/integrations/?kind=backup')
|
return redirect('/admin-tools/integrations/?kind=backup')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_integrations')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def save_mail_settings(request):
|
def save_mail_settings(request):
|
||||||
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
||||||
@@ -1971,8 +2161,7 @@ def save_mail_settings(request):
|
|||||||
return redirect('/admin-tools/integrations/?kind=mail')
|
return redirect('/admin-tools/integrations/?kind=mail')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_integrations')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def save_email_routing_settings(request):
|
def save_email_routing_settings(request):
|
||||||
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
||||||
@@ -2039,8 +2228,7 @@ def save_email_routing_settings(request):
|
|||||||
return redirect('/admin-tools/integrations/?kind=emails')
|
return redirect('/admin-tools/integrations/?kind=emails')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('manage_integrations')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def save_notification_rules(request):
|
def save_notification_rules(request):
|
||||||
rule_ids = request.POST.getlist('rule_ids')
|
rule_ids = request.POST.getlist('rule_ids')
|
||||||
@@ -2103,8 +2291,7 @@ def save_notification_rules(request):
|
|||||||
return redirect('/admin-tools/integrations/?kind=emails')
|
return redirect('/admin-tools/integrations/?kind=emails')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('delete_requests')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def delete_request_from_dashboard(request, kind: str, request_id: int):
|
def delete_request_from_dashboard(request, kind: str, request_id: int):
|
||||||
if kind == 'onboarding':
|
if kind == 'onboarding':
|
||||||
@@ -2122,8 +2309,7 @@ def delete_request_from_dashboard(request, kind: str, request_id: int):
|
|||||||
return redirect('requests_dashboard')
|
return redirect('requests_dashboard')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@_require_capability('retry_requests')
|
||||||
@user_passes_test(_is_staff)
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def retry_request_from_dashboard(request, kind: str, request_id: int):
|
def retry_request_from_dashboard(request, kind: str, request_id: int):
|
||||||
if kind == 'onboarding':
|
if kind == 'onboarding':
|
||||||
|
|||||||
Reference in New Issue
Block a user