Add Telegram notification integration for geofencing
Features: - Multi-channel notifications (Email + Telegram) - User-configurable notification settings per channel - Telegram bot integration with rich messages, location pins, and inline buttons - QR code generation for easy bot access (@myidbot support) - Admin UI for notification settings management - Test functionality for Telegram connection - Comprehensive documentation Implementation: - lib/telegram-service.ts: Telegram API integration - lib/notification-settings-db.ts: Database layer for user notification preferences - lib/geofence-notifications.ts: Extended for parallel multi-channel delivery - API routes for settings management and testing - Admin UI with QR code display and step-by-step instructions - Database table: UserNotificationSettings Documentation: - docs/telegram.md: Technical implementation guide - docs/telegram-anleitung.md: User guide with @myidbot instructions - docs/telegram-setup.md: Admin setup guide - README.md: Updated NPM scripts section Docker: - Updated Dockerfile to copy public directory - Added TELEGRAM_BOT_TOKEN environment variable - Integrated notification settings initialization in db:init 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
208
app/admin/settings/notifications/page.tsx
Normal file
208
app/admin/settings/notifications/page.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
|
||||
export default function NotificationSettingsPage() {
|
||||
const { data: session } = useSession();
|
||||
const [settings, setSettings] = useState({
|
||||
email_enabled: true,
|
||||
telegram_enabled: false,
|
||||
telegram_chat_id: '',
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user) {
|
||||
loadSettings();
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const userId = (session!.user as any).id;
|
||||
const res = await fetch(`/api/users/${userId}/notification-settings`);
|
||||
const data = await res.json();
|
||||
setSettings(data.settings);
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
setLoading(true);
|
||||
setMessage('');
|
||||
|
||||
try {
|
||||
const userId = (session!.user as any).id;
|
||||
const res = await fetch(`/api/users/${userId}/notification-settings`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setMessage('✅ Einstellungen gespeichert!');
|
||||
} else {
|
||||
setMessage('❌ Fehler beim Speichern');
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage('❌ Fehler beim Speichern');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTest() {
|
||||
setLoading(true);
|
||||
setMessage('');
|
||||
|
||||
try {
|
||||
const userId = (session!.user as any).id;
|
||||
const res = await fetch(
|
||||
`/api/users/${userId}/notification-settings/test`,
|
||||
{ method: 'POST' }
|
||||
);
|
||||
|
||||
if (res.ok) {
|
||||
setMessage('✅ Test-Nachricht gesendet!');
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setMessage(`❌ Fehler: ${data.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage('❌ Fehler beim Senden der Test-Nachricht');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto p-6">
|
||||
<h1 className="text-2xl font-bold mb-6">Benachrichtigungseinstellungen</h1>
|
||||
|
||||
<div className="space-y-6 bg-white p-6 rounded-lg shadow">
|
||||
{/* Email Settings */}
|
||||
<div>
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.email_enabled}
|
||||
onChange={(e) =>
|
||||
setSettings({ ...settings, email_enabled: e.target.checked })
|
||||
}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<span className="font-medium">📧 E-Mail Benachrichtigungen</span>
|
||||
</label>
|
||||
<p className="text-sm text-gray-600 ml-7 mt-1">
|
||||
Geofence-Ereignisse per E-Mail erhalten
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Telegram Settings */}
|
||||
<div>
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.telegram_enabled}
|
||||
onChange={(e) =>
|
||||
setSettings({ ...settings, telegram_enabled: e.target.checked })
|
||||
}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<span className="font-medium">📱 Telegram Benachrichtigungen</span>
|
||||
</label>
|
||||
<p className="text-sm text-gray-600 ml-7 mt-1">
|
||||
Geofence-Ereignisse per Telegram erhalten (inkl. Karte und Buttons)
|
||||
</p>
|
||||
|
||||
{settings.telegram_enabled && (
|
||||
<div className="ml-7 mt-3">
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Telegram Chat ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.telegram_chat_id}
|
||||
onChange={(e) =>
|
||||
setSettings({ ...settings, telegram_chat_id: e.target.value })
|
||||
}
|
||||
placeholder="z.B. 123456789"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Deine Telegram Chat ID findest du über @userinfobot
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3 pt-4 border-t">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Speichern...' : 'Speichern'}
|
||||
</button>
|
||||
|
||||
{settings.telegram_enabled && settings.telegram_chat_id && (
|
||||
<button
|
||||
onClick={handleTest}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
Telegram Test
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status Message */}
|
||||
{message && (
|
||||
<div className="p-3 bg-gray-100 rounded-md text-sm">{message}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Help Section */}
|
||||
<div className="mt-6 p-4 bg-blue-50 rounded-lg">
|
||||
<h3 className="font-medium mb-3">📱 Telegram Bot aktivieren</h3>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
{/* QR Code */}
|
||||
<div className="flex flex-col items-center justify-center bg-white p-4 rounded-lg">
|
||||
<p className="text-sm font-medium mb-2">Schritt 1: Bot starten</p>
|
||||
<img
|
||||
src="/telegram-bot-qr.png"
|
||||
alt="Telegram Bot QR Code"
|
||||
className="w-48 h-48 mb-2"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 text-center">
|
||||
Scanne den QR-Code mit deinem Smartphone
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
oder öffne: <a href="https://t.me/MeinTracking_bot" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">@MeinTracking_bot</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="flex flex-col justify-center">
|
||||
<p className="text-sm font-medium mb-2">Schritt 2: Chat ID holen</p>
|
||||
<ol className="text-sm space-y-2 list-decimal list-inside text-gray-700">
|
||||
<li>Starte den Bot mit <code className="bg-gray-200 px-1 rounded">/start</code></li>
|
||||
<li>Suche in Telegram nach <code className="bg-gray-200 px-1 rounded">@myidbot</code></li>
|
||||
<li>Sende <code className="bg-gray-200 px-1 rounded">/getid</code> an @myidbot</li>
|
||||
<li>Der Bot antwortet mit deiner Chat ID</li>
|
||||
<li>Kopiere die Nummer und trage sie oben ein</li>
|
||||
</ol>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Alternative: <code className="bg-gray-200 px-1 rounded">@userinfobot</code> mit <code className="bg-gray-200 px-1 rounded">/start</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
75
app/api/users/[id]/notification-settings/route.ts
Normal file
75
app/api/users/[id]/notification-settings/route.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import {
|
||||
getUserNotificationSettings,
|
||||
updateUserNotificationSettings,
|
||||
} from '@/lib/notification-settings-db';
|
||||
|
||||
// GET /api/users/[id]/notification-settings
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id: userId } = await params;
|
||||
const currentUserId = (session.user as any).id;
|
||||
|
||||
// Users can only view their own settings
|
||||
if (userId !== currentUserId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const settings = getUserNotificationSettings(userId);
|
||||
|
||||
return NextResponse.json({ settings });
|
||||
} catch (error) {
|
||||
console.error('[GET /api/users/[id]/notification-settings]', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to get settings' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH /api/users/[id]/notification-settings
|
||||
export async function PATCH(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id: userId } = await params;
|
||||
const currentUserId = (session.user as any).id;
|
||||
|
||||
// Users can only update their own settings
|
||||
if (userId !== currentUserId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { email_enabled, telegram_enabled, telegram_chat_id } = body;
|
||||
|
||||
const settings = updateUserNotificationSettings(userId, {
|
||||
email_enabled,
|
||||
telegram_enabled,
|
||||
telegram_chat_id,
|
||||
});
|
||||
|
||||
return NextResponse.json({ settings });
|
||||
} catch (error) {
|
||||
console.error('[PATCH /api/users/[id]/notification-settings]', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update settings' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
52
app/api/users/[id]/notification-settings/test/route.ts
Normal file
52
app/api/users/[id]/notification-settings/test/route.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import { telegramService } from '@/lib/telegram-service';
|
||||
import { getUserNotificationSettings } from '@/lib/notification-settings-db';
|
||||
|
||||
// POST /api/users/[id]/notification-settings/test
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id: userId } = await params;
|
||||
const currentUserId = (session.user as any).id;
|
||||
|
||||
if (userId !== currentUserId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const settings = getUserNotificationSettings(userId);
|
||||
|
||||
if (!settings.telegram_chat_id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No Telegram chat ID configured' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const success = await telegramService.testConnection(
|
||||
settings.telegram_chat_id
|
||||
);
|
||||
|
||||
if (success) {
|
||||
return NextResponse.json({ success: true, message: 'Test message sent' });
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to send test message' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[POST /api/users/[id]/notification-settings/test]', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to send test message' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user