AstraOS Ver. 1.43
All checks were successful
Deploy AstraOS Panel / deploy (push) Successful in 16s

Deaktivierung von Account und Notfication System hinzugefügt
This commit is contained in:
elias 2026-08-18 18:47:46 +02:00
parent 0b414b0900
commit 4c3709b64e
5 changed files with 973 additions and 16 deletions

View file

@ -0,0 +1,190 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AstraOS — Account deaktiviert</title>
<link rel="icon" href="/static/media/AstraOS_Logo.ico" type="image/x-icon">
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<style>
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #0a0a0f; }
::-webkit-scrollbar-thumb { background-color: #4b5563; border-radius: 4px; }
body {
background: radial-gradient(circle at top, #1b1b26, #0a0a0f 70%);
min-height: 100vh;
color: #e6e6e6;
overflow-x: hidden;
}
.animated-bg {
position: fixed;
top: 0; left: 0;
width: 100vw; height: 100vh;
z-index: 0;
pointer-events: none;
overflow: hidden;
}
.dot {
position: absolute;
width: 4px; height: 4px;
border-radius: 50%;
background: rgba(192, 132, 252, 0.15);
animation: float 20s infinite linear;
}
@keyframes float {
0% { transform: translateY(100vh) scale(1); }
100% { transform: translateY(-10vh) scale(1.5); }
}
.card-glow {
box-shadow: 0 0 60px rgba(139, 92, 246, 0.08), 0 0 120px rgba(139, 92, 246, 0.04);
}
@keyframes pulseIcon {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.06); }
}
.pulse-icon {
animation: pulseIcon 3s ease-in-out infinite;
}
.toast-container {
position: fixed;
top: 1.25rem;
right: 1.25rem;
z-index: 99999;
display: flex;
flex-direction: column;
gap: 0.6rem;
align-items: flex-end;
}
</style>
</head>
<body class="text-white font-sans antialiased flex items-center justify-center min-h-screen">
<div class="animated-bg"></div>
<!-- Flash messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="toast-container" x-data>
{% for category, message in messages %}
<div x-data="{ show: true }" x-init="setTimeout(() => show = false, 5000)" x-show="show"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-x-8"
x-transition:enter-end="opacity-100 translate-x-0"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="bg-gray-800 shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 p-4 max-w-sm border border-{{ 'red' if category == 'danger' else ('green' if category == 'success' else 'blue') }}-500/40">
<p class="text-sm text-gray-100">{{ message }}</p>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<div class="relative z-10 w-full max-w-md mx-auto px-4" x-data="{ showForm: false }" id="mainCard">
<div class="bg-black/50 backdrop-blur-xl rounded-2xl p-10 card-glow border border-purple-500/20 text-center">
<!-- Icon -->
<div class="pulse-icon inline-flex items-center justify-center w-20 h-20 rounded-full bg-red-500/10 border border-red-500/30 mb-6 mx-auto">
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
</div>
<!-- Title -->
<h1 class="text-3xl font-bold text-white mb-2">Account deaktiviert</h1>
<p class="text-gray-400 text-sm mb-8 leading-relaxed">
Dein AstraOS-Account wurde deaktiviert.<br>
Du hast aktuell keinen Zugang zum Panel.
</p>
<!-- Separator -->
<div class="border-t border-purple-500/20 mb-8"></div>
<!-- Options -->
<div class="space-y-3" x-show="!showForm">
<button @click="showForm = true"
class="w-full py-3 px-4 bg-purple-600/80 hover:bg-purple-600 text-white font-semibold rounded-xl transition-all duration-200 flex items-center justify-center gap-2 hover:shadow-lg hover:shadow-purple-500/20">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"/>
</svg>
Reaktivierung anfragen
</button>
<a href="/login"
class="block w-full py-3 px-4 bg-gray-700/60 hover:bg-gray-700 text-gray-300 font-medium rounded-xl transition-all duration-200 text-center">
Zur Anmeldeseite
</a>
</div>
<!-- Reactivation Form -->
<div x-show="showForm" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 -translate-y-2" x-transition:enter-end="opacity-100 translate-y-0" style="display: none;">
<h2 class="text-lg font-semibold text-purple-300 mb-4 text-left">Reaktivierung anfragen</h2>
<form action="/request_reactivation" method="POST" class="space-y-4 text-left">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div>
<label class="block text-sm font-medium text-gray-300 mb-1">Dein Benutzername</label>
<input type="text" name="username" required placeholder="Dein AstraOS-Benutzername"
class="w-full px-3 py-2.5 bg-gray-800/80 border border-gray-700 rounded-lg text-white text-sm
focus:outline-none focus:ring-2 focus:ring-purple-500 placeholder-gray-600">
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-1">
Begru&#x0308;ndung
<span class="text-gray-500 font-normal">(optional)</span>
</label>
<textarea name="reason" rows="3"
placeholder="Warum soll dein Account reaktiviert werden?"
class="w-full px-3 py-2.5 bg-gray-800/80 border border-gray-700 rounded-lg text-white text-sm resize-none
focus:outline-none focus:ring-2 focus:ring-purple-500 placeholder-gray-600"></textarea>
</div>
<div class="bg-indigo-950/50 border border-indigo-500/30 rounded-lg p-3 text-xs text-indigo-300">
Deine Anfrage wird dem Administrator gemeldet und per Discord zugestellt.
</div>
<div class="flex gap-3 pt-1">
<button type="button" @click="showForm = false"
class="flex-1 py-2.5 rounded-lg text-gray-300 bg-gray-700/60 hover:bg-gray-700 transition text-sm font-medium">
Abbrechen
</button>
<button type="submit"
class="flex-1 py-2.5 bg-purple-600 hover:bg-purple-700 text-white font-semibold rounded-lg transition text-sm">
Anfrage senden
</button>
</div>
</form>
</div>
</div>
<!-- Footer -->
<p class="text-center text-gray-600 text-xs mt-6">
&copy; 2026 AstraOS &nbsp;|&nbsp;
<a href="/impressum" class="hover:text-gray-400 transition">Impressum</a>
</p>
</div>
<script>
const bg = document.querySelector('.animated-bg');
if (bg) {
for (let i = 0; i < 40; i++) {
const dot = document.createElement('div');
dot.classList.add('dot');
dot.style.left = `${Math.random() * 100}vw`;
dot.style.animationDelay = `${Math.random() * -20}s`;
dot.style.animationDuration = `${20 + Math.random() * 10}s`;
bg.appendChild(dot);
}
}
gsap.from('#mainCard', { opacity: 0, y: 30, duration: 0.6, ease: 'power2.out' });
</script>
</body>
</html>

View file

@ -0,0 +1,288 @@
{% extends "sidebar.html" %}
{% block title %}AstraOS — Benachrichtigungen{% endblock %}
{% block content %}
<div x-data="{
showSendModal: false,
sending: false,
form: { title: '', message: '', type: 'info' },
notifications: {{ notifications | tojson | safe }},
async sendNotification() {
if (!this.form.title || !this.form.message) return;
this.sending = true;
try {
const resp = await fetch('/api/notifications/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': document.querySelector('meta[name=csrf-token]').content },
body: JSON.stringify(this.form)
});
const data = await resp.json();
if (data.success) {
this.showSendModal = false;
this.form = { title: '', message: '', type: 'info' };
window.location.reload();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
} catch(e) { alert('Netzwerkfehler'); }
this.sending = false;
},
async markRead(id, index) {
await fetch('/api/notifications/' + id + '/read', {
method: 'POST',
headers: { 'X-CSRFToken': document.querySelector('meta[name=csrf-token]').content }
});
this.notifications[index].is_read = true;
this.updateBadge();
},
async takeAction(id, action, index) {
const resp = await fetch('/api/notifications/' + id + '/action', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': document.querySelector('meta[name=csrf-token]').content },
body: JSON.stringify({ action })
});
const data = await resp.json();
if (data.success) {
this.notifications[index].action_status = action;
this.notifications[index].is_read = true;
this.updateBadge();
}
},
async deleteNotif(id, index) {
if (!confirm('Diese Benachrichtigung wirklich loschen?')) return;
await fetch('/api/notifications/' + id + '/delete', {
method: 'POST',
headers: { 'X-CSRFToken': document.querySelector('meta[name=csrf-token]').content }
});
this.notifications.splice(index, 1);
},
updateBadge() {
const unread = this.notifications.filter(n => !n.is_read).length;
const badge = document.getElementById('notif-badge');
if (badge) { badge.textContent = unread; badge.style.display = unread > 0 ? 'flex' : 'none'; }
},
typeLabel(t) {
return { info: 'Info', warning: 'Warnung', action: 'Aktion', reactivation_request: 'Reaktivierungsanfrage' }[t] || t;
},
actionLabel(s) {
return { pending: 'Ausstehend', approved: 'Genehmigt', rejected: 'Abgelehnt' }[s] || s;
},
formatDate(d) {
if (!d) return 'N/A';
const dt = new Date(d['$date'] !== undefined ? d['$date'] : d);
return isNaN(dt.getTime()) ? String(d) : dt.toLocaleString('de-DE');
}
}" class="space-y-8">
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- Header -->
<header class="flex justify-between items-center">
<div>
<h2 class="text-4xl font-bold tracking-wide {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">
Benachrichtigungen
</h2>
<p class="text-gray-400 mt-1 text-sm">
<span x-text="notifications.filter(n => !n.is_read).length"></span> ungelesen &middot;
<span x-text="notifications.length"></span> gesamt
</p>
</div>
<button @click="showSendModal = true"
class="font-bold py-2 px-5 rounded-lg transition text-white flex items-center gap-2
{% if maintenance_active %}bg-yellow-600 hover:bg-yellow-700{% else %}bg-purple-600 hover:bg-purple-700{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/>
</svg>
Benachrichtigung senden
</button>
</header>
<!-- Notification List -->
<div class="space-y-4">
<template x-if="notifications.length === 0">
<div class="text-center py-20 text-gray-500 bg-black/30 rounded-xl border {% if maintenance_active %}border-yellow-500/20{% else %}border-purple-500/20{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-16 h-16 mx-auto mb-4 opacity-30" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
</svg>
<p class="text-lg">Keine Benachrichtigungen vorhanden.</p>
</div>
</template>
<template x-for="(notif, index) in notifications" :key="notif._id">
<div class="relative rounded-xl p-5 backdrop-blur-lg transition-all duration-300 border"
:class="notif.is_read
? 'bg-black/20 border-gray-700/40'
: '{% if maintenance_active %}bg-yellow-500/5 border-yellow-500/40 shadow-yellow-500/10{% else %}bg-purple-500/5 border-purple-500/40 shadow-purple-500/10{% endif %} shadow-lg'">
<!-- Unread dot -->
<div x-show="!notif.is_read"
class="absolute top-4 right-4 w-2.5 h-2.5 rounded-full {% if maintenance_active %}bg-yellow-400{% else %}bg-purple-400{% endif %} animate-pulse">
</div>
<div class="flex items-start gap-4">
<!-- Type Icon -->
<div class="flex-shrink-0 w-11 h-11 rounded-full flex items-center justify-center text-xl"
:class="{
'bg-blue-500/20': notif.type === 'info',
'bg-orange-500/20': notif.type === 'warning',
'bg-red-500/20': notif.type === 'action',
'bg-yellow-500/20': notif.type === 'reactivation_request'
}">
<span x-text="{ info: '&#128226;', warning: '&#9888;&#65039;', action: '&#128276;', reactivation_request: '&#128275;' }[notif.type] || '&#128226;'"></span>
</div>
<div class="flex-1 min-w-0">
<!-- Title row -->
<div class="flex items-center gap-3 flex-wrap mb-1">
<h3 class="font-semibold text-white text-base" x-text="notif.title"></h3>
<span class="px-2 py-0.5 rounded-full text-xs font-medium"
:class="{
'bg-blue-500/20 text-blue-300': notif.type === 'info',
'bg-orange-500/20 text-orange-300': notif.type === 'warning',
'bg-red-500/20 text-red-300': notif.type === 'action',
'bg-yellow-500/20 text-yellow-300': notif.type === 'reactivation_request'
}"
x-text="typeLabel(notif.type)">
</span>
<template x-if="notif.action_status">
<span class="px-2 py-0.5 rounded-full text-xs font-medium"
:class="{
'bg-yellow-500/20 text-yellow-300': notif.action_status === 'pending',
'bg-green-500/20 text-green-300': notif.action_status === 'approved',
'bg-red-500/20 text-red-300': notif.action_status === 'rejected'
}"
x-text="actionLabel(notif.action_status)">
</span>
</template>
</div>
<!-- Message -->
<p class="text-gray-300 text-sm whitespace-pre-wrap mb-3" x-text="notif.message"></p>
<!-- Meta -->
<div class="flex items-center gap-4 text-xs text-gray-500 flex-wrap">
<span>Von: <span class="text-gray-400" x-text="notif.created_by"></span></span>
<span>Am: <span class="text-gray-400" x-text="formatDate(notif.created_at)"></span></span>
<template x-if="notif.action_taken_by">
<span>Bearbeitet von: <span class="text-gray-400" x-text="notif.action_taken_by"></span></span>
</template>
</div>
<!-- Action Buttons -->
<div class="flex items-center gap-2 mt-3 flex-wrap">
<!-- Approve/Reject for pending action notifications -->
<template x-if="notif.requires_action && notif.action_status === 'pending'">
<div class="flex gap-2">
<button @click.stop="takeAction(notif._id, 'approved', index)"
class="px-3 py-1.5 bg-green-600 hover:bg-green-500 text-white text-xs font-semibold rounded-lg transition">
Genehmigen
</button>
<button @click.stop="takeAction(notif._id, 'rejected', index)"
class="px-3 py-1.5 bg-red-600 hover:bg-red-500 text-white text-xs font-semibold rounded-lg transition">
Ablehnen
</button>
</div>
</template>
<!-- Mark as read -->
<template x-if="!notif.is_read">
<button @click.stop="markRead(notif._id, index)"
class="px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-gray-300 text-xs rounded-lg transition">
Als gelesen markieren
</button>
</template>
<!-- Delete -->
<button @click.stop="deleteNotif(notif._id, index)"
class="px-3 py-1.5 bg-red-900/40 hover:bg-red-800/60 text-red-400 text-xs rounded-lg transition ml-auto">
Loschen
</button>
</div>
</div>
</div>
</div>
</template>
</div>
<!-- Send Notification Modal -->
<div x-show="showSendModal" @keydown.escape.window="showSendModal = false"
class="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50"
style="display: none;">
<div @click.away="showSendModal = false"
class="bg-gray-900 rounded-xl shadow-2xl p-8 w-full max-w-lg
{% if maintenance_active %}border border-yellow-500/50{% else %}border border-purple-500/50{% endif %}">
<h3 class="text-2xl font-bold mb-6 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">
Benachrichtigung senden
</h3>
<div class="space-y-4">
<!-- Type -->
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Typ</label>
<div class="flex gap-2 flex-wrap">
<button type="button" @click="form.type = 'info'"
:class="form.type === 'info' ? '{% if maintenance_active %}bg-yellow-600 border-yellow-500{% else %}bg-purple-600 border-purple-500{% endif %} text-white' : 'bg-gray-800 text-gray-300 border-gray-700 hover:border-gray-500'"
class="px-3 py-1.5 rounded-lg border text-xs font-medium transition">
Info
</button>
<button type="button" @click="form.type = 'warning'"
:class="form.type === 'warning' ? 'bg-orange-600 border-orange-500 text-white' : 'bg-gray-800 text-gray-300 border-gray-700 hover:border-gray-500'"
class="px-3 py-1.5 rounded-lg border text-xs font-medium transition">
Warnung
</button>
<button type="button" @click="form.type = 'action'"
:class="form.type === 'action' ? 'bg-red-600 border-red-500 text-white' : 'bg-gray-800 text-gray-300 border-gray-700 hover:border-gray-500'"
class="px-3 py-1.5 rounded-lg border text-xs font-medium transition">
Aktion (mit Approve/Reject-Buttons)
</button>
</div>
<p x-show="form.type === 'action'" class="text-xs text-yellow-400 mt-1">
Aktions-Benachrichtigungen enthalten interaktive Buttons im Discord und im Panel.
</p>
</div>
<!-- Title -->
<div>
<label class="block text-sm font-medium text-gray-300 mb-1">Titel</label>
<input type="text" x-model="form.title" placeholder="Kurzer, pragnanter Titel..."
class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white text-sm
focus:outline-none focus:ring-2 {% if maintenance_active %}focus:ring-yellow-500{% else %}focus:ring-purple-500{% endif %}">
</div>
<!-- Message -->
<div>
<label class="block text-sm font-medium text-gray-300 mb-1">Nachricht</label>
<textarea x-model="form.message" rows="4" placeholder="Detaillierte Nachricht..."
class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white text-sm resize-none
focus:outline-none focus:ring-2 {% if maintenance_active %}focus:ring-yellow-500{% else %}focus:ring-purple-500{% endif %}"></textarea>
</div>
<!-- Discord hint -->
<div class="bg-indigo-950/50 border border-indigo-500/30 rounded-lg p-3 text-xs text-indigo-300">
Der AstraOS Helper Discord Bot wird dem Owner automatisch eine DM senden.
<span x-show="form.type === 'action'"> Mit interaktiven Genehmigen/Ablehnen-Buttons!</span>
</div>
<!-- Actions -->
<div class="flex justify-end gap-3 pt-2">
<button @click="showSendModal = false; form = { title: '', message: '', type: 'info' }"
class="px-4 py-2 rounded-lg text-gray-300 hover:bg-gray-700 transition text-sm">
Abbrechen
</button>
<button @click="sendNotification()" :disabled="sending || !form.title || !form.message"
class="px-5 py-2 font-semibold rounded-lg transition text-white text-sm flex items-center gap-2 disabled:opacity-50
{% if maintenance_active %}bg-yellow-600 hover:bg-yellow-700{% else %}bg-purple-600 hover:bg-purple-700{% endif %}">
<svg x-show="sending" class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span x-text="sending ? 'Wird gesendet...' : 'Senden'"></span>
</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -228,12 +228,26 @@
📜 Logs 📜 Logs
</a> </a>
{% endif %} {% endif %}
{% if 'maintenance_management' in session.get('permissions', []) %}
<a href="{{ url_for('maintenance_management') }}" <a href="{{ url_for('maintenance_management') }}"
class="block px-3 py-1 rounded-md transition class="block px-3 py-1 rounded-md transition
{% if active_page == 'maintenance_management' %}{% if maintenance_active %}bg-yellow-500/10 text-yellow-300{% else %}bg-purple-500/10 text-purple-300{% endif %}{% else %} {% if active_page == 'maintenance_management' %}{% if maintenance_active %}bg-yellow-500/10 text-yellow-300{% else %}bg-purple-500/10 text-purple-300{% endif %}{% else %}
{% if maintenance_active %}hover:bg-yellow-500/10 hover:text-yellow-300{% else %}hover:bg-purple-500/10 hover:text-purple-300{% endif %}{% endif %}"> {% if maintenance_active %}hover:bg-yellow-500/10 hover:text-yellow-300{% else %}hover:bg-purple-500/10 hover:text-purple-300{% endif %}{% endif %}">
🔧 Maintenance 🔧 Maintenance
</a> </a>
{% endif %}
{% if 'notifications' in session.get('permissions', []) %}
<a href="{{ url_for('notifications_page') }}"
class="block px-3 py-1 rounded-md transition flex items-center justify-between
{% if active_page == 'notifications' %}{% if maintenance_active %}bg-yellow-500/10 text-yellow-300{% else %}bg-purple-500/10 text-purple-300{% endif %}{% else %}
{% if maintenance_active %}hover:bg-yellow-500/10 hover:text-yellow-300{% else %}hover:bg-purple-500/10 hover:text-purple-300{% endif %}{% endif %}">
<span>🔔 Benachrichtigungen</span>
<span id="notif-badge"
class="{% if maintenance_active %}bg-yellow-500{% else %}bg-purple-500{% endif %} text-white text-xs font-bold rounded-full w-5 h-5 items-center justify-center hidden">
0
</span>
</a>
{% endif %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
@ -394,6 +408,26 @@
<script>document.body.classList.add('maintenance-active');</script> <script>document.body.classList.add('maintenance-active');</script>
{% endif %} {% endif %}
{% if 'notifications' in session.get('permissions', []) %}
<script>
// Load unread notification count for badge
function refreshNotifBadge() {
fetch('/api/notifications/unread_count')
.then(r => r.json())
.then(data => {
const badge = document.getElementById('notif-badge');
if (badge) {
badge.textContent = data.count;
badge.style.display = data.count > 0 ? 'flex' : 'none';
}
})
.catch(() => {});
}
refreshNotifBadge();
setInterval(refreshNotifBadge, 30000);
</script>
{% endif %}
<script> <script>
gsap.to("#star", { gsap.to("#star", {
rotation: 360, rotation: 360,

View file

@ -9,8 +9,10 @@
<div x-data="{ <div x-data="{
showAddUserModal: false, showAddUserModal: false,
showDeleteModal: false, showDeleteModal: false,
showDeactivateModal: false,
userToDelete: null, userToDelete: null,
userToEdit: null, userToEdit: null,
userToToggle: null,
usersData: {}, usersData: {},
init() { init() {
this.usersData = JSON.parse(document.getElementById('users-data').textContent); this.usersData = JSON.parse(document.getElementById('users-data').textContent);
@ -18,6 +20,15 @@
openEditModal(userId) { openEditModal(userId) {
this.userToEdit = this.usersData[userId]; this.userToEdit = this.usersData[userId];
this.showAddUserModal = true; this.showAddUserModal = true;
},
openDeactivateModal(userId, username, isActive) {
this.userToToggle = { id: userId, username: username, is_active: isActive };
if (isActive) {
this.showDeactivateModal = true;
} else {
// Direct activate — no reason needed
document.getElementById('toggle-form-' + userId).submit();
}
} }
}"> }">
<div class="space-y-10"> <div class="space-y-10">
@ -37,6 +48,7 @@
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Benutzername</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Benutzername</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Rolle</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Rolle</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Discord</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Discord</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Status</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Erstellt am</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Erstellt am</th>
<th scope="col" class="relative px-6 py-3"> <th scope="col" class="relative px-6 py-3">
<span class="sr-only">Aktionen</span> <span class="sr-only">Aktionen</span>
@ -55,9 +67,26 @@
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">Nicht verknüpft</span> <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">Nicht verknüpft</span>
{% endif %} {% endif %}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
{% if user_table_item.get('is_active', True) %}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-500/20 text-green-300">Aktiv</span>
{% else %}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-500/20 text-red-300">Deaktiviert</span>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">{{ user_table_item.creation_date.strftime('%d.%m.%Y') if user_table_item.creation_date else 'N/A' }}</td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">{{ user_table_item.creation_date.strftime('%d.%m.%Y') if user_table_item.creation_date else 'N/A' }}</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button @click="openEditModal('{{ user_table_item._id | string }}')" class="mr-4 {% if maintenance_active %}text-yellow-400 hover:text-yellow-300{% else %}text-indigo-400 hover:text-indigo-300{% endif %}">Bearbeiten</button> <button @click="openEditModal('{{ user_table_item._id | string }}')" class="mr-3 {% if maintenance_active %}text-yellow-400 hover:text-yellow-300{% else %}text-indigo-400 hover:text-indigo-300{% endif %}">Bearbeiten</button>
{% if session.get('user_id') != user_table_item._id | string %}
{% if user_table_item.get('is_active', True) %}
<button @click="openDeactivateModal('{{ user_table_item._id }}', '{{ user_table_item.username }}', true)" class="mr-3 text-orange-400 hover:text-orange-300">Deaktivieren</button>
{% else %}
<form id="toggle-form-{{ user_table_item._id }}" action="/toggle_user_status/{{ user_table_item._id }}" method="POST" class="inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
</form>
<button @click="openDeactivateModal('{{ user_table_item._id }}', '{{ user_table_item.username }}', false)" class="mr-3 text-green-400 hover:text-green-300">Aktivieren</button>
{% endif %}
{% endif %}
<button @click="userToDelete = '{{ user_table_item._id }}'; showDeleteModal = true" class="text-red-400 hover:text-red-300">Löschen</button> <button @click="userToDelete = '{{ user_table_item._id }}'; showDeleteModal = true" class="text-red-400 hover:text-red-300">Löschen</button>
</td> </td>
</tr> </tr>
@ -121,6 +150,32 @@
</form> </form>
</div> </div>
</div> </div>
<!-- Deactivate Confirm Modal -->
<div x-show="showDeactivateModal" @keydown.escape.window="showDeactivateModal = false"
class="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50"
style="display: none;">
<div @click.away="showDeactivateModal = false"
class="bg-gray-900 border border-orange-500/50 rounded-xl shadow-2xl p-8 w-full max-w-md">
<h3 class="text-2xl font-bold text-orange-400 mb-2">Benutzer deaktivieren</h3>
<p class="text-gray-300 mb-5">Benutzer <span class="font-semibold text-white" x-text="userToToggle ? userToToggle.username : ''"></span> wird deaktiviert. Der Account kann sich nicht mehr anmelden.</p>
<form :action="userToToggle ? '/toggle_user_status/' + userToToggle.id : '#'" method="POST" class="space-y-4">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div>
<label class="block text-sm font-medium text-gray-300 mb-1">Grund (optional)</label>
<input type="text" name="deactivation_reason" placeholder="z.B. Verstos gegen Regeln..."
class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-orange-500">
</div>
<div class="flex justify-end gap-4">
<button type="button" @click="showDeactivateModal = false"
class="px-4 py-2 rounded-lg text-gray-300 hover:bg-gray-700 transition">Abbrechen</button>
<button type="submit"
class="px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white font-semibold rounded-lg transition">Deaktivieren</button>
</div>
</form>
</div>
</div>
<div x-show="showDeleteModal" @keydown.escape.window="showDeleteModal = false" class="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50" style="display: none;"> <div x-show="showDeleteModal" @keydown.escape.window="showDeleteModal = false" class="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50" style="display: none;">
<div @click.away="showDeleteModal = false" class="bg-gray-900 border border-red-500/50 rounded-xl shadow-2xl p-8 w-full max-w-md"> <div @click.away="showDeleteModal = false" class="bg-gray-900 border border-red-500/50 rounded-xl shadow-2xl p-8 w-full max-w-md">
<h3 class="text-2xl font-bold text-red-400 mb-4">Benutzer löschen</h3> <h3 class="text-2xl font-bold text-red-400 mb-4">Benutzer löschen</h3>

View file

@ -16,6 +16,9 @@ from werkzeug.utils import secure_filename
import requests import requests
import socket import socket
import zipfile import zipfile
import asyncio
import discord
from discord.ui import Button, View
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv() load_dotenv()
async_mode = "eventlet" async_mode = "eventlet"
@ -74,6 +77,7 @@ try:
ets2_dlcs_collection = db["ets2_dlcs_packages"] ets2_dlcs_collection = db["ets2_dlcs_packages"]
ip_analyzer_results_collection = db["ip_analyzer_results"] ip_analyzer_results_collection = db["ip_analyzer_results"]
age_verification_collection = db["age_verification"] age_verification_collection = db["age_verification"]
notifications_collection = db["notifications"]
print("INFO: MongoDB-Verbindung erfolgreich hergestellt.") print("INFO: MongoDB-Verbindung erfolgreich hergestellt.")
AVAILABLE_PERMISSIONS = [ AVAILABLE_PERMISSIONS = [
"view_dashboard", "view_dashboard",
@ -92,7 +96,8 @@ try:
"view_ip_analyzer", "view_ip_analyzer",
"view_age_verification", "view_age_verification",
"manage_age_verification", "manage_age_verification",
"view_ayrantracker" "view_ayrantracker",
"notifications"
] ]
if roles_collection.count_documents({}) == 0: if roles_collection.count_documents({}) == 0:
print("WARNUNG: Keine Rollen in der Datenbank gefunden. Erstelle Standard-Rollen...") print("WARNUNG: Keine Rollen in der Datenbank gefunden. Erstelle Standard-Rollen...")
@ -126,9 +131,20 @@ try:
'creation_date': datetime.utcnow(), 'creation_date': datetime.utcnow(),
'discord_id': None, 'discord_id': None,
'discord_username': None, 'discord_username': None,
'discord_avatar': None 'discord_avatar': None,
'is_active': True,
'deactivation_reason': None,
'deactivated_at': None,
'deactivated_by': None
}) })
print("INFO: Standard-Admin-Benutzer ('admin', Passwort: '1234') mit Rolle 'Owner' wurde erstellt.") print("INFO: Standard-Admin-Benutzer ('admin', Passwort: '1234') mit Rolle 'Owner' wurde erstellt.")
else:
# Bestehende User mit is_active-Feld migrieren falls fehlend
accounts_collection.update_many(
{'is_active': {'$exists': False}},
{'$set': {'is_active': True, 'deactivation_reason': None, 'deactivated_at': None, 'deactivated_by': None}}
)
print("INFO: Bestehende Accounts wurden mit is_active-Feld migriert.")
if dashboard_collection.count_documents({}) == 0: if dashboard_collection.count_documents({}) == 0:
print("INFO: Leere dashboard_stats Collection - lege Standardstatistiken an.") print("INFO: Leere dashboard_stats Collection - lege Standardstatistiken an.")
dashboard_collection.insert_many([ dashboard_collection.insert_many([
@ -150,7 +166,7 @@ except Exception as e:
discord_bots_collection = None discord_bots_collection = None
ets2_dlcs_collection = None ets2_dlcs_collection = None
ip_analyzer_results_collection = None ip_analyzer_results_collection = None
print(f"FATAL: Verbindung zur MongoDB fehlgeschlagen: {e}") notifications_collection = None
db = None db = None
accounts_collection = None accounts_collection = None
logs_collection = None logs_collection = None
@ -255,6 +271,160 @@ def stream_bot_output(bot_id, process):
socketio.emit('status_update', {'bot_id': bot_id, 'status': 'stopped'}, room=bot_id) socketio.emit('status_update', {'bot_id': bot_id, 'status': 'stopped'}, room=bot_id)
socketio.emit('console_output', {'bot_id': bot_id, 'type': 'system', 'data': '--- Bot-Prozess beendet ---'}, room=bot_id) socketio.emit('console_output', {'bot_id': bot_id, 'type': 'system', 'data': '--- Bot-Prozess beendet ---'}, room=bot_id)
# ============================================================
# Discord Notification Bot (Lightweight Thread)
# ============================================================
discord_bot_client = None
discord_bot_loop = None
def _get_owner_discord_ids():
"""Returns a list of discord_ids for all users with the Owner role."""
if accounts_collection is None or roles_collection is None:
return []
owner_role = roles_collection.find_one({'name': 'Owner'})
if not owner_role:
return []
owners = accounts_collection.find({'role_id': owner_role['_id'], 'discord_id': {'$ne': None}})
return [o['discord_id'] for o in owners if o.get('discord_id')]
def send_discord_notification(notification_id: str, title: str, message: str, notification_type: str = 'info', requires_action: bool = False):
"""Send a Discord DM to the Owner with optional action buttons."""
global discord_bot_loop
if discord_bot_loop is None or not discord_bot_loop.is_running():
print("WARNUNG: Discord Bot Loop nicht aktiv, Benachrichtigung wird nicht gesendet.")
return
asyncio.run_coroutine_threadsafe(
_async_send_notification(notification_id, title, message, notification_type, requires_action),
discord_bot_loop
)
async def _async_send_notification(notification_id: str, title: str, message: str, notification_type: str, requires_action: bool):
global discord_bot_client
if discord_bot_client is None:
return
owner_discord_ids = _get_owner_discord_ids()
if not owner_discord_ids:
print("WARNUNG: Keine Owner-Discord-ID gefunden.")
return
type_colors = {
'info': discord.Color.blurple(),
'warning': discord.Color.orange(),
'action': discord.Color.red(),
'reactivation_request': discord.Color.yellow()
}
type_emojis = {
'info': '📢',
'warning': '⚠️',
'action': '🔔',
'reactivation_request': '🔓'
}
color = type_colors.get(notification_type, discord.Color.blurple())
emoji = type_emojis.get(notification_type, '📢')
embed = discord.Embed(
title=f"{emoji} {title}",
description=message,
color=color,
timestamp=datetime.utcnow()
)
embed.set_footer(text="AstraOS Admin Panel")
for discord_id_str in owner_discord_ids:
try:
user = await discord_bot_client.fetch_user(int(discord_id_str))
if requires_action:
view = NotificationActionView(notification_id)
await user.send(embed=embed, view=view)
else:
await user.send(embed=embed)
except Exception as e:
print(f"FEHLER: Discord DM an {discord_id_str} fehlgeschlagen: {e}")
class NotificationActionView(View):
def __init__(self, notification_id: str):
super().__init__(timeout=None)
self.notification_id = notification_id
approve_btn = Button(
label="✅ Genehmigen",
style=discord.ButtonStyle.green,
custom_id=f"notif_approve_{notification_id}"
)
reject_btn = Button(
label="❌ Ablehnen",
style=discord.ButtonStyle.red,
custom_id=f"notif_reject_{notification_id}"
)
approve_btn.callback = self.approve_callback
reject_btn.callback = self.reject_callback
self.add_item(approve_btn)
self.add_item(reject_btn)
async def approve_callback(self, interaction: discord.Interaction):
await self._handle_action(interaction, 'approved')
async def reject_callback(self, interaction: discord.Interaction):
await self._handle_action(interaction, 'rejected')
async def _handle_action(self, interaction: discord.Interaction, action: str):
if notifications_collection is not None:
notifications_collection.update_one(
{'_id': ObjectId(self.notification_id)},
{'$set': {
'action_status': action,
'action_taken_by': f"Discord:{interaction.user.name}",
'action_taken_at': datetime.utcnow(),
'is_read': True
}}
)
# If it was a reactivation request, handle account activation
notif = notifications_collection.find_one({'_id': ObjectId(self.notification_id)})
if notif and notif.get('type') == 'reactivation_request' and action == 'approved':
user_id = notif.get('metadata', {}).get('user_id')
if user_id and accounts_collection is not None:
accounts_collection.update_one(
{'_id': ObjectId(user_id)},
{'$set': {'is_active': True, 'deactivation_reason': None, 'deactivated_at': None, 'deactivated_by': None}}
)
label = "✅ Genehmigt" if action == 'approved' else "❌ Abgelehnt"
color = discord.Color.green() if action == 'approved' else discord.Color.red()
result_embed = discord.Embed(
title=f"Aktion ausgeführt: {label}",
description=f"Du hast diese Anfrage **{label.lower()}**.",
color=color,
timestamp=datetime.utcnow()
)
result_embed.set_footer(text="AstraOS Admin Panel")
for child in self.children:
child.disabled = True
await interaction.response.edit_message(embed=result_embed, view=self)
def _run_discord_bot():
global discord_bot_client, discord_bot_loop
intents = discord.Intents.default()
discord_bot_client = discord.Client(intents=intents)
@discord_bot_client.event
async def on_ready():
print(f"INFO: Discord Notification Bot eingeloggt als {discord_bot_client.user}")
token = os.environ.get('DISCORD_BOT_TOKEN')
if not token:
print("WARNUNG: DISCORD_BOT_TOKEN nicht gesetzt. Discord-Bot wird nicht gestartet.")
return
try:
discord_bot_loop = asyncio.new_event_loop()
asyncio.set_event_loop(discord_bot_loop)
discord_bot_loop.run_until_complete(discord_bot_client.start(token))
except Exception as e:
print(f"FEHLER: Discord Bot konnte nicht gestartet werden: {e}")
_discord_bot_thread = threading.Thread(target=_run_discord_bot, daemon=True)
_discord_bot_thread.start()
# ============================================================
if os.environ.get('WERKZEUG_RUN_MAIN') != 'true': if os.environ.get('WERKZEUG_RUN_MAIN') != 'true':
with app.app_context(): with app.app_context():
start_bots_on_startup() start_bots_on_startup()
@ -448,6 +618,10 @@ def login_required(f):
flash("Benutzer nicht gefunden.", "danger") flash("Benutzer nicht gefunden.", "danger")
return redirect(url_for('login')) return redirect(url_for('login'))
if not user.get('is_active', True):
session.clear()
return redirect(url_for('account_disabled'))
if not user.get('discord_id') and request.endpoint not in ['link_discord', 'login_discord', 'callback_discord', 'logout', 'static']: if not user.get('discord_id') and request.endpoint not in ['link_discord', 'login_discord', 'callback_discord', 'logout', 'static']:
return redirect(url_for('link_discord')) return redirect(url_for('link_discord'))
@ -494,6 +668,8 @@ def login():
return render_template('login.html'), 500 return render_template('login.html'), 500
user = accounts_collection.find_one({'username': username}) user = accounts_collection.find_one({'username': username})
if user and check_password_hash(user['password'], password): if user and check_password_hash(user['password'], password):
if not user.get('is_active', True):
return redirect(url_for('account_disabled'))
session.permanent = True session.permanent = True
session['user_id'] = str(user['_id']) session['user_id'] = str(user['_id'])
session['username'] = user['username'] session['username'] = user['username']
@ -758,6 +934,220 @@ def delete_user(user_id):
flash("Benutzer wurde erfolgreich gelöscht.", "success") flash("Benutzer wurde erfolgreich gelöscht.", "success")
return redirect(url_for('user_management')) return redirect(url_for('user_management'))
@app.route('/toggle_user_status/<user_id>', methods=['POST'])
@login_required
@permission_required('manage_users')
def toggle_user_status(user_id):
if accounts_collection is None:
flash("Datenbankverbindung nicht verfügbar.", "danger")
return redirect(url_for('user_management'))
if session.get('user_id') == user_id:
flash("Sie können Ihren eigenen Account nicht deaktivieren.", "danger")
return redirect(url_for('user_management'))
user = accounts_collection.find_one({'_id': ObjectId(user_id)})
if not user:
flash("Benutzer nicht gefunden.", "danger")
return redirect(url_for('user_management'))
new_status = not user.get('is_active', True)
update_data = {'is_active': new_status}
if not new_status:
reason = request.form.get('deactivation_reason', '')
update_data['deactivation_reason'] = reason if reason else None
update_data['deactivated_at'] = datetime.utcnow()
update_data['deactivated_by'] = session.get('username')
add_log("Benutzer deaktiviert", f"Benutzername: {user.get('username')}, Grund: {reason or 'Kein Grund angegeben'}")
flash(f"Benutzer '{user.get('username')}' wurde deaktiviert.", "success")
else:
update_data['deactivation_reason'] = None
update_data['deactivated_at'] = None
update_data['deactivated_by'] = None
add_log("Benutzer aktiviert", f"Benutzername: {user.get('username')}")
flash(f"Benutzer '{user.get('username')}' wurde wieder aktiviert.", "success")
accounts_collection.update_one({'_id': ObjectId(user_id)}, {'$set': update_data})
return redirect(url_for('user_management'))
@app.route('/account_disabled')
def account_disabled():
return render_template('account_disabled.html')
@app.route('/request_reactivation', methods=['POST'])
def request_reactivation():
if notifications_collection is None:
flash("Systemfehler. Bitte versuche es später erneut.", "danger")
return redirect(url_for('account_disabled'))
username = request.form.get('username', '').strip()
reason = request.form.get('reason', '').strip()
if not username:
flash("Benutzername ist erforderlich.", "danger")
return redirect(url_for('account_disabled'))
user = accounts_collection.find_one({'username': username}) if accounts_collection else None
if not user:
flash("Benutzername nicht gefunden.", "danger")
return redirect(url_for('account_disabled'))
if user.get('is_active', True):
flash("Dieser Account ist nicht deaktiviert.", "info")
return redirect(url_for('login'))
# Check for existing pending request
existing = notifications_collection.find_one({
'type': 'reactivation_request',
'action_status': 'pending',
'metadata.user_id': str(user['_id'])
})
if existing:
flash("Es gibt bereits eine offene Anfrage für diesen Account. Bitte warte auf eine Antwort.", "info")
return redirect(url_for('account_disabled'))
notif_doc = {
'type': 'reactivation_request',
'title': f'Reaktivierungsanfrage: {username}',
'message': f'Der Benutzer **{username}** möchte seinen Account reaktivieren.\n\n'
f'**Begründung:** {reason if reason else "Keine Begründung angegeben"}\n\n'
f'**Deaktiviert am:** {user.get("deactivated_at", "Unbekannt")}\n'
f'**Deaktiviert von:** {user.get("deactivated_by", "Unbekannt")}\n'
f'**Deaktivierungsgrund:** {user.get("deactivation_reason") or "Kein Grund angegeben"}',
'created_at': datetime.utcnow(),
'created_by': 'system',
'is_read': False,
'requires_action': True,
'action_status': 'pending',
'action_taken_by': None,
'action_taken_at': None,
'metadata': {'user_id': str(user['_id']), 'username': username},
'discord_message_id': None
}
result = notifications_collection.insert_one(notif_doc)
send_discord_notification(
str(result.inserted_id),
notif_doc['title'],
notif_doc['message'],
notification_type='reactivation_request',
requires_action=True
)
flash("Deine Anfrage wurde gesendet. Ein Administrator wird sich darum kümmern.", "success")
return redirect(url_for('account_disabled'))
# ============================================================
# Notifications Routes
# ============================================================
@app.route('/notifications')
@login_required
@permission_required('notifications')
def notifications_page():
if notifications_collection is None:
flash("Datenbankverbindung nicht verfügbar.", "danger")
return redirect(url_for('dashboard'))
all_notifications = list(notifications_collection.find().sort('created_at', -1))
for n in all_notifications:
n['_id'] = str(n['_id'])
unread_count = notifications_collection.count_documents({'is_read': False})
return render_template('notifications.html', active_page='notifications',
notifications=all_notifications, unread_count=unread_count)
@app.route('/api/notifications/unread_count')
@login_required
def api_notifications_unread_count():
if 'notifications' not in session.get('permissions', []):
return jsonify({'count': 0})
if notifications_collection is None:
return jsonify({'count': 0})
count = notifications_collection.count_documents({'is_read': False})
return jsonify({'count': count})
@app.route('/api/notifications/send', methods=['POST'])
@login_required
@permission_required('notifications')
def api_send_notification():
if notifications_collection is None:
return jsonify({'success': False, 'error': 'DB nicht verfügbar'}), 500
data = request.get_json()
title = (data.get('title') or '').strip()
message = (data.get('message') or '').strip()
notif_type = data.get('type', 'info')
if not title or not message:
return jsonify({'success': False, 'error': 'Titel und Nachricht sind erforderlich.'}), 400
if notif_type not in ['info', 'warning', 'action']:
notif_type = 'info'
requires_action = notif_type == 'action'
notif_doc = {
'type': notif_type,
'title': title,
'message': message,
'created_at': datetime.utcnow(),
'created_by': session.get('username', 'Unbekannt'),
'is_read': False,
'requires_action': requires_action,
'action_status': 'pending' if requires_action else None,
'action_taken_by': None,
'action_taken_at': None,
'metadata': {},
'discord_message_id': None
}
result = notifications_collection.insert_one(notif_doc)
add_log("Benachrichtigung gesendet", f"Titel: {title}, Typ: {notif_type}")
send_discord_notification(
str(result.inserted_id),
title,
message,
notification_type=notif_type,
requires_action=requires_action
)
return jsonify({'success': True, 'id': str(result.inserted_id)})
@app.route('/api/notifications/<notif_id>/action', methods=['POST'])
@login_required
@permission_required('notifications')
def api_notification_action(notif_id):
if notifications_collection is None:
return jsonify({'success': False, 'error': 'DB nicht verfügbar'}), 500
data = request.get_json()
action = data.get('action')
if action not in ['approved', 'rejected']:
return jsonify({'success': False, 'error': 'Ungültige Aktion'}), 400
notif = notifications_collection.find_one({'_id': ObjectId(notif_id)})
if not notif:
return jsonify({'success': False, 'error': 'Benachrichtigung nicht gefunden'}), 404
notifications_collection.update_one(
{'_id': ObjectId(notif_id)},
{'$set': {
'action_status': action,
'action_taken_by': session.get('username'),
'action_taken_at': datetime.utcnow(),
'is_read': True
}}
)
if notif.get('type') == 'reactivation_request' and action == 'approved':
user_id = notif.get('metadata', {}).get('user_id')
if user_id and accounts_collection is not None:
accounts_collection.update_one(
{'_id': ObjectId(user_id)},
{'$set': {'is_active': True, 'deactivation_reason': None, 'deactivated_at': None, 'deactivated_by': None}}
)
add_log("Account reaktiviert (via Panel)", f"User-ID: {user_id}")
add_log("Benachrichtigung bearbeitet", f"ID: {notif_id}, Aktion: {action}")
return jsonify({'success': True})
@app.route('/api/notifications/<notif_id>/read', methods=['POST'])
@login_required
@permission_required('notifications')
def api_notification_mark_read(notif_id):
if notifications_collection is None:
return jsonify({'success': False}), 500
notifications_collection.update_one(
{'_id': ObjectId(notif_id)},
{'$set': {'is_read': True}}
)
return jsonify({'success': True})
@app.route('/api/notifications/<notif_id>/delete', methods=['POST'])
@login_required
@permission_required('notifications')
def api_notification_delete(notif_id):
if notifications_collection is None:
return jsonify({'success': False}), 500
notifications_collection.delete_one({'_id': ObjectId(notif_id)})
add_log("Benachrichtigung gelöscht", f"ID: {notif_id}")
return jsonify({'success': True})
@app.route('/settings', methods=['GET']) @app.route('/settings', methods=['GET'])
@login_required @login_required
def settings(): def settings():