Files
TimeTracker/app/templates/integrations/list.html
T
Dries Peeters 5d4e693a2b Add LDAP setup wizard on Integrations and admin routes
Introduce a guided LDAP configuration wizard mirroring the OIDC flow:
five-step UI with server/TLS, bind, directory layout, groups and
AUTH_METHOD, then optional connection test and .env / Docker Compose
generation for copy-paste deployment.

- Refactor LDAPService.test_connection to accept an optional config
  mapping so the wizard can test draft values without merging live env
  secrets; keep POST /admin/ldap/test on current_app.config.
- Add GET /admin/ldap/setup-wizard plus POST endpoints for test,
  validate, and generate-config (manage_settings, rate limited).
- Surface an LDAP card with status badge and wizard link on the
  integrations list for admins and manage_settings users.
- Add tests for validate, generate, and wizard test delegation.
2026-04-27 20:21:34 +02:00

176 lines
9.8 KiB
HTML

{% extends "base.html" %}
{% from "components/ui.html" import page_header, empty_state %}
{% block title %}{{ _('Integrations') }} - {{ app_name }}{% endblock %}
{% block content %}
{% set breadcrumbs = [
{'text': 'Integrations'}
] %}
{{ page_header(
icon_class='fas fa-plug',
title_text='Integrations',
subtitle_text='Connect with third-party services to extend functionality',
breadcrumbs=breadcrumbs
) }}
<div class="bg-card-light dark:bg-card-dark p-6 rounded-xl border border-border-light dark:border-border-dark shadow-sm mb-6">
<p class="text-sm text-text-muted-light dark:text-text-muted-dark mb-4">
{{ _('Connect your Time Tracker with external services. Configure integrations below to sync data, automate workflows, and enhance productivity.') }}
</p>
</div>
{% if current_user.is_admin or current_user.has_permission('manage_oidc') %}
<!-- OIDC Authentication Setup -->
<div class="bg-card-light dark:bg-card-dark rounded-xl border border-border-light dark:border-border-dark shadow-sm hover:shadow-lg transition-shadow overflow-hidden mb-6 border-2 border-primary/20">
<div class="p-6">
<div class="flex flex-col sm:flex-row items-start justify-between gap-3 mb-4">
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
<i class="fas fa-shield-alt text-primary text-xl"></i>
</div>
<div class="min-w-0 flex-1">
<h3 class="text-lg font-semibold text-text-light dark:text-text-dark">{{ _('OIDC / Single Sign-On') }}</h3>
<p class="text-sm text-text-muted-light dark:text-text-muted-dark mt-1">
{{ _('Configure OpenID Connect authentication for Single Sign-On (SSO) with your identity provider') }}
</p>
</div>
</div>
</div>
<div class="flex items-center gap-2 mb-4">
{% set auth_method = config.get('AUTH_METHOD', 'local') or 'local' %}
{% set oidc_enabled = auth_method.lower() in ['oidc', 'both', 'all'] %}
{% if oidc_enabled %}
<span class="px-2 py-1 text-xs rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 font-medium">
<i class="fas fa-check-circle mr-1"></i>{{ _('Enabled') }}
</span>
{% else %}
<span class="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200 font-medium">
<i class="fas fa-circle mr-1"></i>{{ _('Not Configured') }}
</span>
{% endif %}
</div>
<a href="{{ url_for('admin.oidc_setup_wizard') }}"
class="block w-full bg-primary hover:bg-primary/90 text-white px-4 py-2.5 min-h-[44px] rounded-lg transition-colors text-center font-medium inline-flex items-center justify-center">
<i class="fas fa-magic mr-2"></i>{{ _('Setup Wizard') }}
</a>
</div>
</div>
{% endif %}
{% if current_user.is_admin or current_user.has_permission('manage_settings') %}
<!-- LDAP authentication setup -->
<div class="bg-card-light dark:bg-card-dark rounded-xl border border-border-light dark:border-border-dark shadow-sm hover:shadow-lg transition-shadow overflow-hidden mb-6 border-2 border-primary/20">
<div class="p-6">
<div class="flex flex-col sm:flex-row items-start justify-between gap-3 mb-4">
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
<i class="fas fa-address-book text-primary text-xl"></i>
</div>
<div class="min-w-0 flex-1">
<h3 class="text-lg font-semibold text-text-light dark:text-text-dark">{{ _('LDAP') }}</h3>
<p class="text-sm text-text-muted-light dark:text-text-muted-dark mt-1">
{{ _('Configure directory authentication via environment variables; use the wizard to build a working .env snippet and test connectivity.') }}
</p>
</div>
</div>
</div>
<div class="flex items-center gap-2 mb-4">
{% set auth_method_ldap = config.get('AUTH_METHOD', 'local') or 'local' %}
{% set ldap_auth_enabled = auth_method_ldap.lower() in ['ldap', 'all'] %}
{% if ldap_auth_enabled %}
<span class="px-2 py-1 text-xs rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 font-medium">
<i class="fas fa-check-circle mr-1"></i>{{ _('Enabled') }}
</span>
{% else %}
<span class="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200 font-medium">
<i class="fas fa-circle mr-1"></i>{{ _('Not configured') }}
</span>
{% endif %}
</div>
<a href="{{ url_for('admin.ldap_setup_wizard') }}"
class="block w-full bg-primary hover:bg-primary/90 text-white px-4 py-2.5 min-h-[44px] rounded-lg transition-colors text-center font-medium inline-flex items-center justify-center">
<i class="fas fa-magic mr-2"></i>{{ _('Setup Wizard') }}
</a>
</div>
</div>
{% endif %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
{% for provider in available_providers %}
{% set existing_integration = integrations|selectattr('provider', 'equalto', provider.provider)|first if integrations else none %}
<div class="bg-card-light dark:bg-card-dark rounded-xl border border-border-light dark:border-border-dark shadow-sm hover:shadow-lg transition-shadow overflow-hidden">
<div class="p-6">
<div class="flex items-start justify-between mb-4">
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
<i class="fas fa-{{ provider.icon }} text-primary text-xl"></i>
</div>
<div class="min-w-0 flex-1">
<h3 class="text-lg font-semibold text-text-light dark:text-text-dark truncate">{{ provider.display_name }}</h3>
{% if provider.description %}
<p class="text-sm text-text-muted-light dark:text-text-muted-dark mt-1 line-clamp-2">{{ provider.description }}</p>
{% endif %}
</div>
</div>
</div>
<div class="flex items-center gap-2 mb-4">
{% if existing_integration %}
{% if existing_integration.is_active %}
<span class="px-2 py-1 text-xs rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 font-medium">
<i class="fas fa-check-circle mr-1"></i>{{ _('Connected') }}
</span>
{% else %}
<span class="px-2 py-1 text-xs rounded-full bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 font-medium">
<i class="fas fa-exclamation-circle mr-1"></i>{{ _('Not Connected') }}
</span>
{% endif %}
{% if existing_integration.is_global %}
<span class="px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
{{ _('Global') }}
</span>
{% endif %}
{% if existing_integration.last_sync_at %}
<span class="text-xs text-text-muted-light dark:text-text-muted-dark">
{{ _('Synced') }} {{ existing_integration.last_sync_at|user_date }}
</span>
{% endif %}
{% else %}
<span class="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200 font-medium">
<i class="fas fa-circle mr-1"></i>{{ _('Not Set Up') }}
</span>
{% endif %}
</div>
<div class="space-y-2">
{% if has_setup_wizard(provider.provider) %}
<a href="{{ url_for('integrations.setup_wizard', provider=provider.provider) }}"
class="block w-full bg-primary hover:bg-primary/90 text-white px-4 py-2.5 rounded-lg transition-colors text-center font-medium">
<i class="fas fa-magic mr-2"></i>{{ _('Setup Wizard') }}
</a>
{% endif %}
<a href="{{ url_for('integrations.manage_integration', provider=provider.provider) }}"
class="block w-full {% if existing_integration and existing_integration.is_active %}bg-gray-600 hover:bg-gray-700{% else %}{% if has_setup_wizard(provider.provider) %}bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600{% else %}bg-primary hover:bg-primary/90{% endif %}{% endif %} text-white px-4 py-2.5 rounded-lg transition-colors text-center font-medium">
{% if existing_integration %}
{% if existing_integration.is_active %}
<i class="fas fa-cog mr-2"></i>{{ _('Manage') }}
{% else %}
<i class="fas fa-link mr-2"></i>{{ _('Connect') }}
{% endif %}
{% else %}
<i class="fas fa-plus mr-2"></i>{{ _('Setup') }}
{% endif %}
</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}