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:
2026-02-11 11:30:20 +01:00
parent b50594dbfa
commit f14515e9cf
94 changed files with 21601 additions and 5111 deletions

View File

@@ -0,0 +1,151 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Einladung annehmen - LinkedIn Posts</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'brand': {
'bg': '#3d4848',
'bg-light': '#4a5858',
'bg-dark': '#2d3838',
'highlight': '#ffc700',
'highlight-dark': '#e6b300',
},
'linkedin': '#0A66C2'
}
}
}
}
</script>
<style>
body { background-color: #3d4848; }
.btn-linkedin { background-color: #0A66C2; }
.btn-linkedin:hover { background-color: #004182; }
.card-bg { background-color: #4a5858; border-color: #5a6868; }
.input-bg { background-color: #3d4848; border-color: #5a6868; }
.input-bg:focus { border-color: #ffc700; outline: none; }
.btn-primary { background-color: #ffc700; color: #2d3838; }
.btn-primary:hover { background-color: #e6b300; }
</style>
</head>
<body class="text-gray-100 min-h-screen flex items-center justify-center">
<div class="w-full max-w-md px-4">
<div class="card-bg rounded-xl border p-8">
<div class="text-center mb-8">
<img src="/static/logo.png" alt="Logo" class="h-16 w-auto mx-auto mb-4">
<h1 class="text-2xl font-bold text-white mb-2">Einladung von {{ company_name }}</h1>
<p class="text-gray-400">Du wurdest von {{ inviter_name }} eingeladen, dem Team beizutreten.</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">
{{ error }}
</div>
{% endif %}
{% if expired %}
<div class="bg-yellow-900/50 border border-yellow-500 text-yellow-200 px-4 py-3 rounded-lg mb-6">
<p class="font-medium">Diese Einladung ist abgelaufen</p>
<p class="text-sm">Bitte frage nach einer neuen Einladung.</p>
</div>
{% else %}
<!-- Invitation Details -->
<div class="bg-brand-bg border border-gray-600 rounded-lg p-4 mb-6">
<div class="flex items-center gap-3 mb-3">
<div class="w-10 h-10 bg-brand-highlight/20 rounded-lg flex items-center justify-center">
<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="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
</svg>
</div>
<div>
<p class="text-white font-medium">{{ company_name }}</p>
<p class="text-sm text-gray-400">Eingeladen am {{ invitation.created_at.strftime('%d.%m.%Y') }}</p>
</div>
</div>
<p class="text-sm text-gray-400">
Als Teammitglied kannst du LinkedIn-Posts erstellen, die der Unternehmensstrategie entsprechen.
</p>
</div>
<!-- LinkedIn OAuth -->
<div class="mb-6">
<a href="/auth/linkedin?invite_token={{ invitation.token }}" class="w-full btn-linkedin text-white font-medium py-3 px-4 rounded-lg transition-colors flex items-center justify-center gap-3">
<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 beitreten
</a>
</div>
<div class="relative my-6">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-600"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-4 bg-brand-bg-light text-gray-400">oder mit E-Mail</span>
</div>
</div>
<!-- Email/Password Form -->
<form method="POST" action="/invite/{{ invitation.token }}/accept" class="space-y-4">
<div>
<label for="email" class="block text-sm font-medium text-gray-300 mb-1">E-Mail</label>
<input type="email" id="email" name="email" required
class="w-full input-bg border rounded-lg px-4 py-2 text-white"
value="{{ invitation.email }}" readonly>
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-300 mb-1">Passwort erstellen</label>
<input type="password" id="password" name="password" required minlength="8"
class="w-full input-bg border rounded-lg px-4 py-2 text-white"
placeholder="Mindestens 8 Zeichen">
<p class="text-xs text-gray-500 mt-1">Mind. 8 Zeichen, 1 Großbuchstabe, 1 Zahl</p>
</div>
<div>
<label for="password_confirm" class="block text-sm font-medium text-gray-300 mb-1">Passwort bestätigen</label>
<input type="password" id="password_confirm" name="password_confirm" required
class="w-full input-bg border rounded-lg px-4 py-2 text-white"
placeholder="Passwort wiederholen">
</div>
<button type="submit" class="w-full btn-primary font-medium py-3 px-4 rounded-lg transition-colors">
Einladung annehmen
</button>
</form>
{% endif %}
<div class="text-center pt-6 mt-6 border-t border-gray-600">
<p class="text-gray-400 text-sm">
Du hast bereits ein Konto?
<a href="/login" class="text-brand-highlight hover:underline">Anmelden</a>
</p>
</div>
</div>
</div>
<script>
// Password confirmation validation
const form = document.querySelector('form');
if (form) {
const password = document.getElementById('password');
const passwordConfirm = document.getElementById('password_confirm');
form.addEventListener('submit', function(e) {
if (password.value !== passwordConfirm.value) {
e.preventDefault();
alert('Passwörter stimmen nicht überein');
}
});
}
</script>
</body>
</html>