Major updates: LinkedIn auto-posting, timezone fixes, and Docker improvements
Features: - Add LinkedIn OAuth integration and auto-posting functionality - Add scheduler service for automated post publishing - Add metadata field to generated_posts for LinkedIn URLs - Add privacy policy page for LinkedIn API compliance - Add company management features and employee accounts - Add license key system for company registrations Fixes: - Fix timezone issues (use UTC consistently across app) - Fix datetime serialization errors in database operations - Fix scheduling timezone conversion (local time to UTC) - Fix import errors (get_database -> db) Infrastructure: - Update Docker setup to use port 8001 (avoid conflicts) - Add SSL support with nginx-proxy and Let's Encrypt - Add LinkedIn setup documentation - Add migration scripts for schema updates Services: - Add linkedin_service.py for LinkedIn API integration - Add scheduler_service.py for background job processing - Add storage_service.py for Supabase Storage - Add email_service.py improvements - Add encryption utilities for token storage Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
316
src/web/templates/user/settings.html
Normal file
316
src/web/templates/user/settings.html
Normal file
@@ -0,0 +1,316 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Einstellungen{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-white mb-2">Einstellungen</h1>
|
||||
<p class="text-gray-400">Verwalte deine Profil- und Email-Einstellungen</p>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
|
||||
<strong>Error:</strong> {{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Email Settings -->
|
||||
<div class="card-bg rounded-xl border p-6 mb-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
Email-Benachrichtigungen
|
||||
</h2>
|
||||
<p class="text-sm text-gray-400 mb-6">
|
||||
Konfiguriere die Email-Adressen für den Freigabe-Workflow. Wenn ein Post in "Bearbeitet" verschoben wird,
|
||||
erhält die Kunden-Email einen Link zur Freigabe. Nach der Entscheidung wird die Creator-Email benachrichtigt.
|
||||
</p>
|
||||
|
||||
<form id="emailSettingsForm" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">
|
||||
Creator-Email
|
||||
<span class="text-gray-500 font-normal">(erhält Benachrichtigungen über Entscheidungen)</span>
|
||||
</label>
|
||||
<input type="email"
|
||||
name="creator_email"
|
||||
id="creatorEmail"
|
||||
value="{{ profile.creator_email or '' }}"
|
||||
placeholder="creator@example.com"
|
||||
class="w-full px-4 py-3 bg-brand-bg border border-brand-bg-light rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-brand-highlight">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">
|
||||
Kunden-Email
|
||||
<span class="text-gray-500 font-normal">(erhält Posts zur Freigabe)</span>
|
||||
</label>
|
||||
<input type="email"
|
||||
name="customer_email"
|
||||
id="customerEmail"
|
||||
value="{{ profile.customer_email or '' }}"
|
||||
placeholder="kunde@example.com"
|
||||
class="w-full px-4 py-3 bg-brand-bg border border-brand-bg-light rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-brand-highlight">
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<button type="submit"
|
||||
id="saveEmailsBtn"
|
||||
class="px-6 py-3 bg-brand-highlight hover:bg-brand-highlight/90 text-brand-bg-dark font-medium rounded-lg transition-colors flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- LinkedIn Account Connection -->
|
||||
<div class="card-bg rounded-xl border border-gray-700 p-6 mb-6">
|
||||
<h2 class="text-xl font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-[#0A66C2]" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
||||
</svg>
|
||||
LinkedIn-Konto verbinden
|
||||
</h2>
|
||||
|
||||
{% if linkedin_account %}
|
||||
<!-- Connected State -->
|
||||
<div class="bg-green-900/20 border border-green-600 rounded-lg p-4 mb-4">
|
||||
<div class="flex items-start gap-4">
|
||||
{% if linkedin_account.linkedin_picture %}
|
||||
<img src="{{ linkedin_account.linkedin_picture }}"
|
||||
alt="{{ linkedin_account.linkedin_name }}"
|
||||
class="w-12 h-12 rounded-full border-2 border-green-500">
|
||||
{% endif %}
|
||||
<div class="flex-1">
|
||||
<p class="text-white font-medium flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
{{ linkedin_account.linkedin_name }}
|
||||
</p>
|
||||
<p class="text-gray-400 text-sm mt-1">
|
||||
Verbunden seit {{ linkedin_account.created_at.strftime('%d.%m.%Y um %H:%M') }} Uhr
|
||||
</p>
|
||||
{% if linkedin_account.last_used_at %}
|
||||
<p class="text-gray-500 text-xs mt-1">
|
||||
Zuletzt verwendet: {{ linkedin_account.last_used_at.strftime('%d.%m.%Y um %H:%M') }} Uhr
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if linkedin_account.last_error %}
|
||||
<div class="bg-yellow-900/20 border border-yellow-600 rounded-lg p-4 mb-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<svg class="w-5 h-5 text-yellow-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-yellow-200 font-medium">Verbindungsproblem</p>
|
||||
<p class="text-yellow-200/80 text-sm mt-1">
|
||||
Deine LinkedIn-Verbindung funktioniert möglicherweise nicht mehr. Bitte verbinde dein Konto erneut.
|
||||
</p>
|
||||
<p class="text-yellow-600 text-xs mt-2 font-mono">{{ linkedin_account.last_error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-blue-900/20 border border-blue-600 rounded-lg p-4 mb-4">
|
||||
<p class="text-blue-200 text-sm">
|
||||
<strong>✨ Automatisches Posten aktiviert!</strong><br>
|
||||
Geplante Posts werden automatisch auf deinem LinkedIn-Profil veröffentlicht.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<button onclick="disconnectLinkedIn()"
|
||||
class="px-6 py-3 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
Verbindung trennen
|
||||
</button>
|
||||
{% else %}
|
||||
<!-- Not Connected State -->
|
||||
<p class="text-gray-400 mb-4">
|
||||
Verbinde dein LinkedIn-Konto, um Posts automatisch zu veröffentlichen.
|
||||
Wenn dein Konto verbunden ist, werden geplante Posts direkt auf dein LinkedIn-Profil gepostet.
|
||||
</p>
|
||||
|
||||
<div class="bg-brand-bg-light rounded-lg p-4 mb-4 border border-brand-bg-light">
|
||||
<div class="flex items-start gap-3">
|
||||
<svg class="w-5 h-5 text-brand-highlight flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<div class="text-sm text-gray-400">
|
||||
<p class="font-medium text-white mb-2">Vorteile:</p>
|
||||
<ul class="space-y-1">
|
||||
<li>• Automatische Veröffentlichung zur geplanten Zeit</li>
|
||||
<li>• Keine manuelle Arbeit mehr nötig</li>
|
||||
<li>• Posts mit Bildern werden unterstützt</li>
|
||||
<li>• Du bleibst im Workflow informiert</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="/settings/linkedin/connect"
|
||||
class="inline-flex items-center gap-2 px-6 py-3 bg-[#0A66C2] hover:bg-[#004182] text-white rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
||||
</svg>
|
||||
Mit LinkedIn verbinden
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Workflow Info -->
|
||||
<div class="card-bg rounded-xl border p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
So funktioniert der Workflow
|
||||
</h2>
|
||||
<div class="space-y-4 text-sm text-gray-400">
|
||||
<div class="flex gap-3">
|
||||
<div class="w-8 h-8 rounded-full bg-yellow-600/20 text-yellow-400 flex items-center justify-center flex-shrink-0 font-semibold">1</div>
|
||||
<div>
|
||||
<p class="text-white font-medium">Vorschlag</p>
|
||||
<p>Neue Posts werden hier erstellt und können bearbeitet werden.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<div class="w-8 h-8 rounded-full bg-blue-600/20 text-blue-400 flex items-center justify-center flex-shrink-0 font-semibold">2</div>
|
||||
<div>
|
||||
<p class="text-white font-medium">Bearbeitet</p>
|
||||
<p>Wenn ein Post hierher verschoben wird, erhält die Kunden-Email eine Freigabe-Anfrage.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<div class="w-8 h-8 rounded-full bg-green-600/20 text-green-400 flex items-center justify-center flex-shrink-0 font-semibold">3</div>
|
||||
<div>
|
||||
<p class="text-white font-medium">Veröffentlicht</p>
|
||||
<p>Nach Freigabe durch den Kunden landet der Post hier und ist bereit für LinkedIn.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.getElementById('emailSettingsForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const btn = document.getElementById('saveEmailsBtn');
|
||||
const originalHTML = btn.innerHTML;
|
||||
btn.innerHTML = '<div class="w-4 h-4 border-2 border-brand-bg-dark border-t-transparent rounded-full animate-spin"></div> Speichern...';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('creator_email', document.getElementById('creatorEmail').value);
|
||||
formData.append('customer_email', document.getElementById('customerEmail').value);
|
||||
|
||||
const response = await fetch('/api/settings/emails', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Fehler beim Speichern');
|
||||
}
|
||||
|
||||
// Show success
|
||||
btn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg> Gespeichert!';
|
||||
btn.classList.remove('bg-brand-highlight');
|
||||
btn.classList.add('bg-green-600');
|
||||
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = originalHTML;
|
||||
btn.classList.remove('bg-green-600');
|
||||
btn.classList.add('bg-brand-highlight');
|
||||
btn.disabled = false;
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
btn.innerHTML = 'Fehler!';
|
||||
btn.classList.remove('bg-brand-highlight');
|
||||
btn.classList.add('bg-red-600');
|
||||
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = originalHTML;
|
||||
btn.classList.remove('bg-red-600');
|
||||
btn.classList.add('bg-brand-highlight');
|
||||
btn.disabled = false;
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
// LinkedIn disconnect
|
||||
async function disconnectLinkedIn() {
|
||||
if (!confirm('LinkedIn-Verbindung wirklich trennen?\n\nPosts werden dann nicht mehr automatisch veröffentlicht und du erhältst wieder Email-Benachrichtigungen.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/settings/linkedin/disconnect', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Fehler beim Trennen der Verbindung. Bitte versuche es erneut.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error disconnecting LinkedIn:', error);
|
||||
alert('Fehler: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Show success/error messages from URL params
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.has('success')) {
|
||||
const successMsg = urlParams.get('success');
|
||||
if (successMsg === 'linkedin_connected') {
|
||||
// Show temporary success message
|
||||
const successDiv = document.createElement('div');
|
||||
successDiv.className = 'fixed top-4 right-4 bg-green-600 text-white px-6 py-3 rounded-lg shadow-lg z-50';
|
||||
successDiv.textContent = '✓ LinkedIn-Konto erfolgreich verbunden!';
|
||||
document.body.appendChild(successDiv);
|
||||
setTimeout(() => successDiv.remove(), 3000);
|
||||
|
||||
// Clean URL
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
}
|
||||
}
|
||||
if (urlParams.has('error')) {
|
||||
const errorMsg = urlParams.get('error');
|
||||
const errorMessages = {
|
||||
'linkedin_auth_failed': 'LinkedIn-Authentifizierung fehlgeschlagen',
|
||||
'invalid_state': 'Sicherheitsprüfung fehlgeschlagen. Bitte versuche es erneut.',
|
||||
'token_exchange_failed': 'Token-Austausch fehlgeschlagen',
|
||||
'userinfo_failed': 'Konnte LinkedIn-Profil nicht abrufen',
|
||||
'connection_failed': 'Verbindung fehlgeschlagen. Bitte versuche es erneut.'
|
||||
};
|
||||
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'fixed top-4 right-4 bg-red-600 text-white px-6 py-3 rounded-lg shadow-lg z-50';
|
||||
errorDiv.textContent = '✗ ' + (errorMessages[errorMsg] || 'Ein Fehler ist aufgetreten');
|
||||
document.body.appendChild(errorDiv);
|
||||
setTimeout(() => errorDiv.remove(), 5000);
|
||||
|
||||
// Clean URL
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user