Files
workdock-platform/backend/workflows/app_registry.py
Md Bayazid Bostame 89cc11e41e
Some checks failed
CI / python-validation (push) Has been cancelled
CI / docker-release-gate (push) Has been cancelled
i18n / compile-translations (push) Has been cancelled
fix: allow super admin customer platform apps
2026-04-01 13:30:49 +02:00

434 lines
15 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from .models import PortalAppConfig
from .roles import ROLE_ADMIN, ROLE_IT_STAFF, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_STAFF, ROLE_SUPER_ADMIN, get_user_role_key, user_has_capability
# The registry controls discoverability and packaging posture for apps.
# Actual authorization still comes from role capabilities in roles.py.
@dataclass(frozen=True)
class AppDefinition:
key: str
section: str
route_name: str
title: object
description: object
action_label: object
capability: str | None = None
accent: str = ''
accent_label: str = 'APP'
style_variant: str = ''
tags: tuple[object, ...] = ()
APP_DEFINITIONS: tuple[AppDefinition, ...] = (
AppDefinition(
key='onboarding',
section=PortalAppConfig.SECTION_APP,
route_name='onboarding_create',
title=_('Onboarding'),
description=_('Neue Mitarbeitende erfassen, PDF mit Briefkopf erstellen, Benachrichtigungen senden und in Nextcloud ablegen.'),
action_label=_('Onboarding starten'),
accent='ON',
tags=(_('Mehrschritt-Formular'), 'PDF', _('E-Mail Routing')),
style_variant='primary',
),
AppDefinition(
key='offboarding',
section=PortalAppConfig.SECTION_APP,
route_name='offboarding_create',
title=_('Offboarding'),
description=_('Mitarbeitende suchen, Daten vorbefüllen, Offboarding-Dokumente erzeugen und Rückgabe-Prozess starten.'),
action_label=_('Offboarding starten'),
accent='OFF',
tags=(_('Profile-Suche'), _('Hardware-Liste'), _('IT-Rückgabe')),
style_variant='red',
),
AppDefinition(
key='requests_dashboard',
section=PortalAppConfig.SECTION_APP,
route_name='requests_dashboard',
title=_('Anfragen Dashboard'),
description=_('Status, Suchfunktion, PDF-Links und Verlauf aller Onboarding-/Offboarding-Anfragen.'),
action_label=_('Dashboard öffnen'),
capability='access_requests_dashboard',
accent='APP',
tags=(_('Suche'), _('Status'), _('PDF Zugriff')),
),
AppDefinition(
key='company_config',
section=PortalAppConfig.SECTION_PLATFORM,
route_name='portal_company_config_page',
title=_('Company Config'),
description=_('Rechtliche Firmendaten, Kontaktpunkte und öffentliche Unternehmenslinks pflegen.'),
action_label=_('Öffnen'),
capability='manage_company_config',
),
AppDefinition(
key='trial_management',
section=PortalAppConfig.SECTION_PLATFORM,
route_name='portal_trial_config_page',
title=_('Trial Management'),
description=_('Testlaufzeit, Banner und sichere Einschränkungen für Demo-Umgebungen steuern.'),
action_label=_('Öffnen'),
capability='manage_trial_lifecycle',
),
AppDefinition(
key='branding',
section=PortalAppConfig.SECTION_PLATFORM,
route_name='portal_branding_page',
title=_('Branding'),
description=_('Logo, Portalname, Farben und PDF-Briefkopf verwalten.'),
action_label=_('Öffnen'),
capability='manage_product_branding',
),
AppDefinition(
key='app_registry',
section=PortalAppConfig.SECTION_PLATFORM,
route_name='portal_app_registry_page',
title=_('App Registry'),
description=_('Apps zentral aktivieren, sortieren und für Kundenauftritte vorbereiten.'),
action_label=_('Öffnen'),
capability='manage_app_registry',
),
AppDefinition(
key='integrations',
section=PortalAppConfig.SECTION_ADMIN,
route_name='integrations_setup_page',
title=_('Integrationen'),
description=_('Nextcloud- und E-Mail-Setup.'),
action_label=_('Öffnen'),
capability='manage_integrations',
),
AppDefinition(
key='job_monitor',
section=PortalAppConfig.SECTION_ADMIN,
route_name='job_monitor_page',
title=_('Job Monitor'),
description=_('Asynchrone Aufgaben, Fehler und letzte Worker-Läufe prüfen.'),
action_label=_('Öffnen'),
capability='view_job_monitor',
),
AppDefinition(
key='users',
section=PortalAppConfig.SECTION_ADMIN,
route_name='user_management_page',
title=_('Benutzer & Rollen'),
description=_('Benutzer anlegen, Rollen zuweisen und Zugriffe steuern.'),
action_label=_('Öffnen'),
capability='manage_users',
),
AppDefinition(
key='audit_log',
section=PortalAppConfig.SECTION_ADMIN,
route_name='audit_log_page',
title=_('Audit Log'),
description=_('Wichtige Admin-Aktionen nachvollziehen und prüfen.'),
action_label=_('Öffnen'),
capability='view_audit_log',
),
AppDefinition(
key='backups',
section=PortalAppConfig.SECTION_ADMIN,
route_name='backup_recovery_page',
title=_('Backup & Recovery'),
description=_('Backups erstellen und sicher verifizieren.'),
action_label=_('Öffnen'),
capability='manage_backups',
),
AppDefinition(
key='welcome_emails',
section=PortalAppConfig.SECTION_ADMIN,
route_name='welcome_emails_page',
title=_('Welcome E-Mails'),
description=_('Geplante Welcome Mails verwalten.'),
action_label=_('Öffnen'),
capability='manage_welcome_emails',
),
AppDefinition(
key='form_builder',
section=PortalAppConfig.SECTION_ADMIN,
route_name='form_builder_page',
title=_('Form Builder'),
description=_('Felder, Schritte und Optionen verwalten.'),
action_label=_('Öffnen'),
capability='manage_builders',
),
AppDefinition(
key='intro_builder',
section=PortalAppConfig.SECTION_ADMIN,
route_name='intro_builder_page',
title=_('Einweisungs-Builder'),
description=_('Checklistenpunkte für das Einweisungsprotokoll konfigurieren.'),
action_label=_('Öffnen'),
capability='manage_builders',
),
AppDefinition(
key='handbook',
section=PortalAppConfig.SECTION_ADMIN,
route_name='handbook_page',
title=_('Handbook'),
description=_('Project wiki and developer documentation in one place.'),
action_label=_('Öffnen'),
capability='view_docs',
),
AppDefinition(
key='django_admin',
section=PortalAppConfig.SECTION_ADMIN,
route_name='admin:index',
title=_('Django Admin'),
description=_('Vollständige Datenverwaltung.'),
action_label=_('Öffnen'),
capability='access_django_admin_link',
),
)
DEFAULT_ROLE_VISIBILITY = {
# These defaults are product recommendations for fresh deployments.
# Saved PortalAppConfig rows can override them per customer installation.
'onboarding': {
ROLE_SUPER_ADMIN: True,
ROLE_ADMIN: True,
ROLE_IT_STAFF: True,
ROLE_STAFF: True,
},
'offboarding': {
ROLE_SUPER_ADMIN: True,
ROLE_ADMIN: True,
ROLE_IT_STAFF: True,
ROLE_STAFF: True,
},
'requests_dashboard': {
ROLE_SUPER_ADMIN: True,
ROLE_ADMIN: True,
ROLE_IT_STAFF: True,
ROLE_STAFF: False,
},
'branding': {
ROLE_SUPER_ADMIN: True,
ROLE_ADMIN: False,
ROLE_IT_STAFF: False,
ROLE_STAFF: False,
},
'company_config': {
ROLE_SUPER_ADMIN: True,
ROLE_ADMIN: False,
ROLE_IT_STAFF: False,
ROLE_STAFF: False,
},
'trial_management': {
ROLE_SUPER_ADMIN: False,
ROLE_ADMIN: False,
ROLE_IT_STAFF: False,
ROLE_STAFF: False,
},
'app_registry': {
ROLE_SUPER_ADMIN: False,
ROLE_ADMIN: False,
ROLE_IT_STAFF: False,
ROLE_STAFF: False,
},
'integrations': {
ROLE_SUPER_ADMIN: True,
ROLE_ADMIN: True,
ROLE_IT_STAFF: False,
ROLE_STAFF: False,
},
'job_monitor': {
ROLE_SUPER_ADMIN: True,
ROLE_ADMIN: True,
ROLE_IT_STAFF: False,
ROLE_STAFF: False,
},
'users': {
ROLE_SUPER_ADMIN: True,
ROLE_ADMIN: False,
ROLE_IT_STAFF: False,
ROLE_STAFF: False,
},
'audit_log': {
ROLE_SUPER_ADMIN: True,
ROLE_ADMIN: True,
ROLE_IT_STAFF: False,
ROLE_STAFF: False,
},
'backups': {
ROLE_SUPER_ADMIN: True,
ROLE_ADMIN: True,
ROLE_IT_STAFF: False,
ROLE_STAFF: False,
},
'welcome_emails': {
ROLE_SUPER_ADMIN: True,
ROLE_ADMIN: True,
ROLE_IT_STAFF: False,
ROLE_STAFF: False,
},
'form_builder': {
ROLE_SUPER_ADMIN: True,
ROLE_ADMIN: True,
ROLE_IT_STAFF: False,
ROLE_STAFF: False,
},
'intro_builder': {
ROLE_SUPER_ADMIN: True,
ROLE_ADMIN: True,
ROLE_IT_STAFF: False,
ROLE_STAFF: False,
},
'handbook': {
ROLE_SUPER_ADMIN: True,
ROLE_ADMIN: True,
ROLE_IT_STAFF: False,
ROLE_STAFF: False,
},
'django_admin': {
ROLE_SUPER_ADMIN: False,
ROLE_ADMIN: False,
ROLE_IT_STAFF: False,
ROLE_STAFF: False,
},
}
def _default_visibility_summary(definition_key: str) -> str:
visibility = DEFAULT_ROLE_VISIBILITY.get(definition_key, {})
enabled_roles = [
role
for role in (ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF)
if visibility.get(role)
]
if not enabled_roles:
return str(_('Nur Platform'))
if enabled_roles == [ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF]:
return str(_('Alle Firmenrollen'))
return ' + '.join(str(ROLE_LABELS[role]) for role in enabled_roles if role in ROLE_LABELS)
SECTION_META = {
PortalAppConfig.SECTION_APP: {
'title': _('Apps'),
'subtitle': _('Wählen Sie den gewünschten Prozess.'),
'css_class': 'section-head-primary',
'grid_class': 'apps-grid',
},
PortalAppConfig.SECTION_PLATFORM: {
'title': _('Platform Apps'),
'subtitle': _('Produktweite Konfiguration und Produktsteuerung.'),
'css_class': 'section-head-platform',
'grid_class': 'admin-grid',
},
PortalAppConfig.SECTION_ADMIN: {
'title': _('Admin Apps'),
'subtitle': _('Konfiguration, Tests und Steuerung.'),
'css_class': 'section-head-admin',
'grid_class': 'admin-grid',
},
}
def ensure_portal_app_configs() -> None:
for index, definition in enumerate(APP_DEFINITIONS):
visibility = DEFAULT_ROLE_VISIBILITY.get(definition.key, {})
PortalAppConfig.objects.get_or_create(
key=definition.key,
defaults={
'section': definition.section,
'sort_order': index,
'is_enabled': True,
'visible_to_super_admin': visibility.get(ROLE_SUPER_ADMIN, False),
'visible_to_admin': visibility.get(ROLE_ADMIN, False),
'visible_to_it_staff': visibility.get(ROLE_IT_STAFF, False),
'visible_to_staff': visibility.get(ROLE_STAFF, False),
},
)
normalize_portal_app_sort_orders()
def normalize_portal_app_sort_orders() -> None:
for section_key, _label in PortalAppConfig.SECTION_CHOICES:
configs = list(PortalAppConfig.objects.filter(section=section_key).order_by('sort_order', 'key'))
for position, config in enumerate(configs):
if config.sort_order != position:
config.sort_order = position
config.save(update_fields=['sort_order'])
def get_portal_app_registry_rows() -> list[dict[str, object]]:
ensure_portal_app_configs()
config_map = {config.key: config for config in PortalAppConfig.objects.all()}
rows: list[dict[str, object]] = []
for index, definition in enumerate(APP_DEFINITIONS):
config = config_map[definition.key]
rows.append(
{
'definition': definition,
'config': config,
'default_section': definition.section,
'default_sort_order': index,
'default_visibility_summary': _default_visibility_summary(definition.key),
}
)
return rows
def build_portal_app_sections(user) -> list[dict[str, object]]:
ensure_portal_app_configs()
config_map = {config.key: config for config in PortalAppConfig.objects.all()}
grouped: dict[str, list[dict[str, object]]] = {key: [] for key in SECTION_META}
role_key = get_user_role_key(user)
for definition in APP_DEFINITIONS:
config = config_map.get(definition.key)
if not config or not config.is_enabled:
continue
if definition.capability and not user_has_capability(user, definition.capability):
continue
if role_key != ROLE_PLATFORM_OWNER:
if role_key == ROLE_SUPER_ADMIN and not config.visible_to_super_admin:
continue
if role_key == ROLE_ADMIN and not config.visible_to_admin:
continue
if role_key == ROLE_IT_STAFF and not config.visible_to_it_staff:
continue
if role_key == ROLE_STAFF and not config.visible_to_staff:
continue
grouped[config.section].append(
{
'key': definition.key,
'href': reverse(definition.route_name),
'title': config.translated_title_override() or str(definition.title),
'description': config.translated_description_override() or str(definition.description),
'action_label': config.translated_action_label_override() or str(definition.action_label),
'accent': definition.accent,
'accent_label': definition.accent_label,
'style_variant': definition.style_variant,
'tags': [str(tag) for tag in definition.tags],
'sort_order': config.sort_order,
}
)
sections: list[dict[str, object]] = []
for section_key, meta in SECTION_META.items():
apps = sorted(grouped.get(section_key, []), key=lambda item: (item['sort_order'], item['title']))
if not apps:
continue
sections.append(
{
'key': section_key,
'title': str(meta['title']),
'subtitle': str(meta['subtitle']),
'css_class': meta['css_class'],
'grid_class': meta['grid_class'],
'apps': apps,
}
)
return sections