Files
workdock-platform/backend/workflows/views.py
Md Bayazid Bostame e47b1b3110
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
feat: add session expiry warning
2026-04-01 22:05:17 +02:00

651 lines
22 KiB
Python

from pathlib import Path
import re
from datetime import timedelta
from tempfile import NamedTemporaryFile
import json
from io import BytesIO
from celery import current_app
from django.conf import settings
from django.db import connection
from django.db import IntegrityError
from django.db.models import Q
from django.db.models import Count
from django.shortcuts import get_object_or_404, redirect, render
from django.contrib import messages
from django.contrib.auth import get_user_model, login as auth_login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.tokens import default_token_generator
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import ensure_csrf_cookie
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 _
from django.urls import reverse
from .app_registry import build_portal_app_sections, get_portal_app_registry_rows, normalize_portal_app_sort_orders
from .backup_ops import (
create_backup_bundle,
delete_backup_bundle,
latest_backup_health_snapshot,
list_backup_bundles,
verify_backup_bundle,
)
from .branding import get_branding_email_copy, get_company_email_domain, get_default_notification_templates, get_portal_trial_config, is_trial_expired
from . import account_views, admin_config_views, integrations_views, request_views
from .admin_section_builders import (
build_branding_sections as _build_branding_sections,
build_company_config_sections as _build_company_config_sections,
)
from .admin_user_helpers import (
render_user_management as _render_user_management,
send_user_access_email as _send_user_access_email,
would_remove_last_platform_owner as _would_remove_last_platform_owner,
would_remove_last_super_admin as _would_remove_last_super_admin,
)
from .forms import AccountAvatarForm, AccountDetailsForm, AccountNotificationPreferencesForm, AccountTOTPDisableForm, AccountTOTPEnableForm, AccountTOTPRegenerateRecoveryCodesForm, AppLoginForm, AppTOTPChallengeForm, OffboardingRequestForm, OnboardingRequestForm, PortalBrandingForm, PortalCompanyConfigForm, PortalTrialConfigForm, UserManagementCreateForm
from .form_builder import (
DEFAULT_FIELD_ORDER,
DEFAULT_CONDITIONAL_RULES,
FORM_PRESETS,
LOCKED_FIELD_RULES,
LOCKED_SECTION_RULES,
OFFBOARDING_PAGE_LABELS,
OFFBOARDING_PAGE_ORDER,
ONBOARDING_DEFAULT_PAGE,
build_custom_field_key,
custom_field_target_key,
ensure_form_field_configs,
ensure_form_conditional_rule_configs,
ensure_form_section_configs,
get_custom_field_configs,
get_custom_section_configs,
get_default_page_map,
get_section_definitions,
get_section_labels,
get_section_order,
apply_form_preset,
)
from .form_builder_views import form_builder_page_impl, form_builder_save_order_impl
from .intro_builder_views import intro_builder_page_impl
from .observability_views import (
audit_log_page_impl,
backup_recovery_page_impl,
create_backup_from_admin_impl,
job_monitor_page_impl,
verify_backup_from_admin_impl,
)
from .view_audit import audit as _audit, audit_action_label as _audit_action_label, display_user_name as _display_user_name
from .view_context import (
form_field_labels as _form_field_labels,
request_custom_field_details as _request_custom_field_details,
request_status_label as _request_status_label,
request_target_label as _request_target_label,
)
from .view_form_runtime import (
CONDITIONAL_RULE_OPERATOR_CHOICES,
ONBOARDING_CHECKBOX_LISTS,
ONBOARDING_GROUPS,
ONBOARDING_INLINE_CHECKS,
active_conditional_target_keys as _active_conditional_target_keys,
build_offboarding_sections as _build_offboarding_sections,
build_onboarding_layout as _build_onboarding_layout,
build_onboarding_sections as _build_onboarding_sections,
conditional_rule_summary as _conditional_rule_summary,
field_rule_summary as _field_rule_summary,
normalized_conditional_rule_payload as _normalized_conditional_rule_payload,
translate_choice_list as _translate_choice_list,
)
from .view_permissions import require_capability as _require_capability
from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormConditionalRuleConfig, FormCustomFieldConfig, FormCustomSectionConfig, FormFieldConfig, FormOption, FormSectionConfig, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, PortalTrialConfig, ScheduledWelcomeEmail, SystemEmailConfig, UserNotification, UserProfile, WorkflowConfig
from .emailing import send_system_email
from .notifications import notify_user
from .roles import ROLE_GROUP_NAMES, ROLE_LABELS, ROLE_PLATFORM_OWNER, 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 .totp import build_otpauth_uri, generate_recovery_codes, generate_totp_secret
from .tasks import (
_generate_onboarding_intro_pdf,
_generate_onboarding_intro_session_pdf,
build_intro_sections_for_request,
process_offboarding_request,
process_onboarding_request,
send_scheduled_welcome_email,
)
def _redirect_back(request, fallback: str):
target = (request.POST.get('next') or request.GET.get('next') or '').strip()
if target.startswith('/'):
return redirect(target)
referer = (request.META.get('HTTP_REFERER') or '').strip()
if referer.startswith('http://127.0.0.1') or referer.startswith('http://localhost') or referer.startswith('/'):
return redirect(referer)
return redirect(fallback)
@login_required
@require_POST
def mark_notification_read(request, notification_id: int):
notification = get_object_or_404(UserNotification, id=notification_id, user=request.user)
notification.mark_read()
return _redirect_back(request, 'home')
@login_required
@require_POST
def mark_all_notifications_read(request):
UserNotification.objects.filter(user=request.user, read_at__isnull=True).update(read_at=timezone.now())
return _redirect_back(request, 'home')
@login_required
@require_POST
def session_keepalive(request):
now_ts = int(timezone.now().timestamp())
request.session['last_activity_ts'] = now_ts
request.session['auth_fresh_ts'] = now_ts
return JsonResponse(
{
'status': 'ok',
'idle_timeout_seconds': settings.SESSION_IDLE_TIMEOUT_SECONDS,
'reauth_timeout_seconds': settings.SENSITIVE_ACTION_REAUTH_SECONDS,
'refreshed_at': now_ts,
}
)
def healthz(request):
db_ok = True
try:
with connection.cursor() as cursor:
cursor.execute('SELECT 1')
cursor.fetchone()
except Exception:
db_ok = False
status_code = 200 if db_ok else 503
return JsonResponse(
{
'status': 'ok' if db_ok else 'degraded',
'service': 'workdock',
'db': 'ok' if db_ok else 'error',
'time': timezone.now().isoformat(),
},
status=status_code,
)
def login_page(request):
return account_views.login_page_impl(request)
def login_totp_page(request):
return account_views.login_totp_page_impl(request)
@login_required
def account_profile_page(request):
return account_views.account_profile_page_impl(request)
@login_required
def home(request):
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
role_key = get_user_role_key(request.user)
return render(
request,
'workflows/home.html',
{
'nextcloud_enabled': is_nextcloud_enabled(),
'email_test_mode': is_email_test_mode(),
'workflow_config': config,
'role_label': get_user_role_label(request.user),
'role_key': role_key,
'portal_app_sections': build_portal_app_sections(request.user),
},
)
@_require_capability('manage_app_registry')
def portal_app_registry_page(request):
return admin_config_views.portal_app_registry_page_impl(request, translate_choice_list=_translate_choice_list)
@_require_capability('view_job_monitor')
def job_monitor_page(request):
return job_monitor_page_impl(request)
@_require_capability('manage_app_registry')
@require_POST
def save_portal_app_registry(request):
return admin_config_views.save_portal_app_registry_impl(request, audit_fn=_audit)
@_require_capability('manage_users')
def user_management_page(request):
return admin_config_views.user_management_page_impl(
request,
render_user_management_fn=lambda req, create_form=None, status_code=200: _render_user_management(
req,
create_form=create_form,
status_code=status_code,
audit_action_label_fn=_audit_action_label,
display_user_name_fn=_display_user_name,
),
)
@_require_capability('manage_product_branding')
def portal_branding_page(request):
return admin_config_views.portal_branding_page_impl(request, build_branding_sections_fn=_build_branding_sections)
@_require_capability('manage_product_branding')
@require_POST
def save_portal_branding(request):
return admin_config_views.save_portal_branding_impl(request, audit_fn=_audit, build_branding_sections_fn=_build_branding_sections)
@_require_capability('manage_company_config')
def portal_company_config_page(request):
return admin_config_views.portal_company_config_page_impl(request, build_company_config_sections_fn=_build_company_config_sections)
@_require_capability('manage_company_config')
@require_POST
def save_portal_company_config(request):
return admin_config_views.save_portal_company_config_impl(request, audit_fn=_audit, build_company_config_sections_fn=_build_company_config_sections)
@_require_capability('manage_trial_lifecycle')
def portal_trial_config_page(request):
return admin_config_views.portal_trial_config_page_impl(request)
@_require_capability('manage_trial_lifecycle')
@require_POST
def save_portal_trial_config(request):
return admin_config_views.save_portal_trial_config_impl(request, audit_fn=_audit)
@_require_capability('manage_users')
@require_POST
def create_user_from_admin(request):
return admin_config_views.create_user_from_admin_impl(
request,
render_user_management_fn=lambda req, create_form=None, status_code=200: _render_user_management(
req,
create_form=create_form,
status_code=status_code,
audit_action_label_fn=_audit_action_label,
display_user_name_fn=_display_user_name,
),
send_user_access_email_fn=lambda req, target_user, invitation: _send_user_access_email(
req,
target_user,
invitation=invitation,
display_user_name_fn=_display_user_name,
),
audit_fn=_audit,
display_user_name_fn=_display_user_name,
)
@_require_capability('manage_users')
@require_POST
def update_user_from_admin(request, user_id: int):
return admin_config_views.update_user_from_admin_impl(
request,
user_id,
would_remove_last_platform_owner_fn=_would_remove_last_platform_owner,
would_remove_last_super_admin_fn=_would_remove_last_super_admin,
audit_fn=_audit,
display_user_name_fn=_display_user_name,
)
@_require_capability('manage_users')
@require_POST
def send_password_reset_from_admin(request, user_id: int):
return admin_config_views.send_password_reset_from_admin_impl(
request,
user_id,
send_user_access_email_fn=lambda req, target_user, invitation: _send_user_access_email(
req,
target_user,
invitation=invitation,
display_user_name_fn=_display_user_name,
),
audit_fn=_audit,
display_user_name_fn=_display_user_name,
)
@_require_capability('manage_users')
@require_POST
def delete_user_from_admin(request, user_id: int):
return admin_config_views.delete_user_from_admin_impl(
request,
user_id,
would_remove_last_platform_owner_fn=_would_remove_last_platform_owner,
would_remove_last_super_admin_fn=_would_remove_last_super_admin,
audit_fn=_audit,
display_user_name_fn=_display_user_name,
)
@_require_capability('view_docs')
def handbook_page(request):
return admin_config_views.handbook_page_impl(request)
@_require_capability('view_docs')
def project_wiki_page(request):
return admin_config_views.project_wiki_page_impl(request)
@_require_capability('view_docs')
def developer_handbook_page(request):
return admin_config_views.developer_handbook_page_impl(request)
@_require_capability('view_docs')
def deployment_hosts_page(request):
return admin_config_views.deployment_hosts_page_impl(request)
@_require_capability('view_docs')
def release_checklist_page(request):
return admin_config_views.release_checklist_page_impl(request)
@_require_capability('view_audit_log')
def audit_log_page(request):
return audit_log_page_impl(request)
@_require_capability('manage_backups')
def backup_recovery_page(request):
return backup_recovery_page_impl(request)
@_require_capability('manage_backups')
@require_POST
def create_backup_from_admin(request):
return create_backup_from_admin_impl(
request,
audit_fn=_audit,
notify_user_fn=notify_user,
create_backup_bundle_fn=create_backup_bundle,
)
@_require_capability('manage_backups')
@require_POST
def verify_backup_from_admin(request, backup_name: str):
return verify_backup_from_admin_impl(
request,
backup_name,
audit_fn=_audit,
notify_user_fn=notify_user,
verify_backup_bundle_fn=verify_backup_bundle,
)
@_require_capability('manage_backups')
@require_POST
def delete_backup_from_admin(request, backup_name: str):
try:
result = delete_backup_bundle(backup_name)
_audit(
request,
'backup_deleted',
target_type='backup_bundle',
target_label=backup_name,
details={},
)
messages.success(request, _('Backup wurde gelöscht: %(name)s') % {'name': result['name']})
except Exception as exc:
messages.error(request, _('Backup konnte nicht gelöscht werden: %(error)s') % {'error': exc})
return redirect('backup_recovery_page')
@_require_capability('view_request_timeline')
def request_timeline_page(request, kind: str, request_id: int):
return request_views.request_timeline_page_impl(
request,
kind,
request_id,
request_target_label_fn=_request_target_label,
request_custom_field_details_fn=_request_custom_field_details,
audit_action_label_fn=_audit_action_label,
)
@login_required
def requests_dashboard(request):
return request_views.requests_dashboard_impl(
request,
audit_fn=_audit,
request_target_label_fn=_request_target_label,
request_status_label_fn=_request_status_label,
)
@login_required
@ensure_csrf_cookie
def onboarding_create(request):
return request_views.onboarding_create_impl(
request,
build_onboarding_layout_fn=_build_onboarding_layout,
build_onboarding_sections_fn=_build_onboarding_sections,
normalized_conditional_rule_payload_fn=_normalized_conditional_rule_payload,
display_user_name_fn=_display_user_name,
onboarding_inline_checks=ONBOARDING_INLINE_CHECKS,
onboarding_checkbox_lists=ONBOARDING_CHECKBOX_LISTS,
)
@login_required
def onboarding_success(request, request_id: int):
return request_views.onboarding_success_impl(request, request_id)
@_require_capability('generate_intro_pdfs')
@require_POST
def generate_onboarding_intro_pdf(request, request_id: int):
return request_views.generate_onboarding_intro_pdf_impl(request, request_id, audit_fn=_audit)
@_require_capability('generate_intro_pdfs')
@require_POST
def generate_onboarding_intro_session_pdf(request, request_id: int):
return request_views.generate_onboarding_intro_session_pdf_impl(request, request_id, audit_fn=_audit, display_user_name_fn=_display_user_name)
@_require_capability('run_intro_session')
def onboarding_intro_session_page(request, request_id: int):
return request_views.onboarding_intro_session_page_impl(request, request_id, audit_fn=_audit, display_user_name_fn=_display_user_name)
@login_required
@ensure_csrf_cookie
def offboarding_create(request):
return request_views.offboarding_create_impl(
request,
build_offboarding_sections_fn=_build_offboarding_sections,
display_user_name_fn=_display_user_name,
)
@login_required
def offboarding_success(request, request_id: int):
return request_views.offboarding_success_impl(request, request_id)
@_require_capability('manage_builders')
def form_builder_page(request):
return form_builder_page_impl(
request,
audit_fn=_audit,
translate_choice_list=_translate_choice_list,
form_field_labels_fn=_form_field_labels,
field_rule_summary_fn=_field_rule_summary,
conditional_rule_summary_fn=_conditional_rule_summary,
onboarding_groups=ONBOARDING_GROUPS,
conditional_rule_operator_choices=CONDITIONAL_RULE_OPERATOR_CHOICES,
)
@_require_capability('manage_builders')
def intro_builder_page(request):
return intro_builder_page_impl(
request,
audit_fn=_audit,
translate_choice_list=_translate_choice_list,
)
@_require_capability('manage_integrations')
def integrations_setup_page(request):
return integrations_views.integrations_setup_page_impl(request)
@_require_capability('manage_welcome_emails')
def welcome_emails_page(request):
return integrations_views.welcome_emails_page_impl(request)
@_require_capability('manage_welcome_emails')
@require_POST
def trigger_welcome_email_now(request, schedule_id: int):
return integrations_views.trigger_welcome_email_now_impl(
request,
schedule_id,
audit_fn=_audit,
send_task_fn=send_scheduled_welcome_email,
)
@_require_capability('manage_welcome_emails')
@require_POST
def save_welcome_email_settings(request):
return integrations_views.save_welcome_email_settings_impl(request, audit_fn=_audit)
@_require_capability('manage_welcome_emails')
@require_POST
def bulk_welcome_email_action(request):
return integrations_views.bulk_welcome_email_action_impl(
request,
audit_fn=_audit,
send_task_fn=send_scheduled_welcome_email,
)
@_require_capability('manage_welcome_emails')
@require_POST
def pause_welcome_email(request, schedule_id: int):
return integrations_views.pause_welcome_email_impl(request, schedule_id, audit_fn=_audit)
@_require_capability('manage_welcome_emails')
@require_POST
def resume_welcome_email(request, schedule_id: int):
return integrations_views.resume_welcome_email_impl(
request,
schedule_id,
audit_fn=_audit,
send_task_fn=send_scheduled_welcome_email,
)
@_require_capability('manage_welcome_emails')
@require_POST
def cancel_welcome_email(request, schedule_id: int):
return integrations_views.cancel_welcome_email_impl(request, schedule_id, audit_fn=_audit)
@_require_capability('manage_builders')
@require_POST
def form_builder_save_order(request):
return form_builder_save_order_impl(request, audit_fn=_audit)
@_require_capability('manage_integrations')
@require_POST
def send_test_email(request):
return integrations_views.send_test_email_impl(request, audit_fn=_audit, redirect_back_fn=_redirect_back)
@_require_capability('manage_integrations')
@require_POST
def nextcloud_test_upload(request):
return integrations_views.nextcloud_test_upload_impl(request, audit_fn=_audit, redirect_back_fn=_redirect_back)
@_require_capability('manage_integrations')
@require_POST
def toggle_nextcloud_enabled(request):
return integrations_views.toggle_nextcloud_enabled_impl(request, audit_fn=_audit, redirect_back_fn=_redirect_back)
@_require_capability('manage_integrations')
@require_POST
def toggle_email_mode(request):
return integrations_views.toggle_email_mode_impl(request, audit_fn=_audit, redirect_back_fn=_redirect_back)
@_require_capability('manage_integrations')
@require_POST
def save_integrations_settings(request):
return integrations_views.save_integrations_settings_impl(request, audit_fn=_audit)
@_require_capability('manage_integrations')
@require_POST
def save_nextcloud_settings(request):
return integrations_views.save_nextcloud_settings_impl(request, audit_fn=_audit)
@_require_capability('manage_integrations')
@require_POST
def save_workflow_rules(request):
return integrations_views.save_workflow_rules_impl(request, audit_fn=_audit)
@_require_capability('manage_integrations')
@require_POST
def save_backup_settings(request):
return integrations_views.save_backup_settings_impl(request, audit_fn=_audit)
@_require_capability('manage_integrations')
@require_POST
def save_mail_settings(request):
return integrations_views.save_mail_settings_impl(request, audit_fn=_audit)
@_require_capability('manage_integrations')
@require_POST
def save_email_routing_settings(request):
return integrations_views.save_email_routing_settings_impl(request, audit_fn=_audit)
@_require_capability('manage_integrations')
@require_POST
def save_notification_rules(request):
return integrations_views.save_notification_rules_impl(request, audit_fn=_audit)
@_require_capability('delete_requests')
@require_POST
def delete_request_from_dashboard(request, kind: str, request_id: int):
return request_views.delete_request_from_dashboard_impl(request, kind, request_id, audit_fn=_audit, request_target_label_fn=_request_target_label)
@_require_capability('retry_requests')
@require_POST
def retry_request_from_dashboard(request, kind: str, request_id: int):
return request_views.retry_request_from_dashboard_impl(request, kind, request_id, audit_fn=_audit, request_target_label_fn=_request_target_label)