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:
@@ -36,6 +36,7 @@ RUN npm ci --omit=dev
|
|||||||
# Kopiere Build Artifacts
|
# Kopiere Build Artifacts
|
||||||
COPY --from=builder /app/.next ./.next
|
COPY --from=builder /app/.next ./.next
|
||||||
COPY --from=builder /app/data ./data
|
COPY --from=builder /app/data ./data
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
# Kopiere App Code (benötigt für instrumentation.ts, lib/, etc.)
|
# Kopiere App Code (benötigt für instrumentation.ts, lib/, etc.)
|
||||||
COPY --from=builder /app/instrumentation.ts ./
|
COPY --from=builder /app/instrumentation.ts ./
|
||||||
|
|||||||
@@ -1093,7 +1093,6 @@ npm run email:dev # Email Template Dev Server
|
|||||||
# Linting
|
# Linting
|
||||||
npm run lint # ESLint ausführen
|
npm run lint # ESLint ausführen
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📚 Automatische Dokumentations-Synchronisation
|
## 📚 Automatische Dokumentations-Synchronisation
|
||||||
|
|||||||
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,8 @@ services:
|
|||||||
- SMTP_FROM_EMAIL=${SMTP_FROM_EMAIL}
|
- SMTP_FROM_EMAIL=${SMTP_FROM_EMAIL}
|
||||||
- SMTP_FROM_NAME=${SMTP_FROM_NAME:-Location Tracker}
|
- SMTP_FROM_NAME=${SMTP_FROM_NAME:-Location Tracker}
|
||||||
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
|
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
|
||||||
|
# Telegram Configuration
|
||||||
|
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./public:/app/public # Mount public directory for static files
|
- ./public:/app/public # Mount public directory for static files
|
||||||
|
|||||||
163
docs/telegram-anleitung.md
Normal file
163
docs/telegram-anleitung.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# 📱 Telegram-Benachrichtigungen aktivieren
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Erhalte Geofence-Benachrichtigungen direkt in Telegram mit:
|
||||||
|
- 📍 Standort-Pin auf der Karte
|
||||||
|
- 🔔 Sofortige Push-Benachrichtigungen
|
||||||
|
- 🔗 Direktlinks zur Karte und zum Dashboard
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Schritt-für-Schritt Anleitung
|
||||||
|
|
||||||
|
### Schritt 1: Bot starten
|
||||||
|
|
||||||
|
#### Option A: QR-Code scannen (empfohlen für Smartphone)
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="../public/telegram-bot-qr.png" width="300" alt="Telegram Bot QR Code">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
1. Öffne die Kamera-App auf deinem Smartphone
|
||||||
|
2. Scanne den QR-Code
|
||||||
|
3. Telegram öffnet sich automatisch
|
||||||
|
4. Klicke auf **"Start"** oder sende `/start`
|
||||||
|
|
||||||
|
#### Option B: Link öffnen
|
||||||
|
|
||||||
|
1. Klicke auf: **[https://t.me/MeinTracking_bot](https://t.me/MeinTracking_bot)**
|
||||||
|
2. Telegram öffnet sich automatisch
|
||||||
|
3. Klicke auf **"Start"** oder sende `/start`
|
||||||
|
|
||||||
|
✅ Du solltest eine Willkommensnachricht vom Bot erhalten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Schritt 2: Chat ID herausfinden
|
||||||
|
|
||||||
|
#### Option A: Mit @myidbot (empfohlen)
|
||||||
|
|
||||||
|
1. Suche in Telegram nach: **@myidbot**
|
||||||
|
2. Sende: `/getid`
|
||||||
|
3. Der Bot antwortet sofort mit deiner Chat ID:
|
||||||
|
```
|
||||||
|
Your user ID: 123456789
|
||||||
|
```
|
||||||
|
4. **Kopiere die Nummer** (z.B. `123456789`)
|
||||||
|
|
||||||
|
#### Option B: Mit @userinfobot
|
||||||
|
|
||||||
|
1. Suche in Telegram nach: **@userinfobot**
|
||||||
|
2. Sende: `/start`
|
||||||
|
3. Der Bot antwortet mit deinen Informationen:
|
||||||
|
```
|
||||||
|
Id: 123456789
|
||||||
|
First name: Dein Name
|
||||||
|
...
|
||||||
|
```
|
||||||
|
4. **Kopiere die Nummer** bei "Id:" (z.B. `123456789`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Schritt 3: Chat ID im Dashboard eintragen
|
||||||
|
|
||||||
|
1. Melde dich im Location Tracker an
|
||||||
|
2. Gehe zu: **Einstellungen** → **Benachrichtigungen**
|
||||||
|
3. Aktiviere: ☑️ **Telegram Benachrichtigungen**
|
||||||
|
4. Trage deine **Chat ID** ein (die Nummer aus Schritt 2)
|
||||||
|
5. Klicke auf **"Speichern"**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Schritt 4: Verbindung testen
|
||||||
|
|
||||||
|
1. Klicke auf den Button **"Telegram Test"**
|
||||||
|
2. Du solltest sofort eine Test-Nachricht in Telegram erhalten:
|
||||||
|
```
|
||||||
|
✅ Telegram Connection Test
|
||||||
|
|
||||||
|
Die Verbindung funktioniert!
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Fertig!** Ab jetzt erhältst du alle Geofence-Benachrichtigungen auch per Telegram.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📨 Beispiel-Benachrichtigung
|
||||||
|
|
||||||
|
So sieht eine Geofence-Benachrichtigung aus:
|
||||||
|
|
||||||
|
```
|
||||||
|
🟢 Geofence BETRETEN
|
||||||
|
|
||||||
|
📱 Device: Mein iPhone
|
||||||
|
📍 Geofence: Zuhause
|
||||||
|
🕐 Zeit: 04.12.25, 14:30
|
||||||
|
📊 Ereignis: Hat Zuhause betreten
|
||||||
|
```
|
||||||
|
|
||||||
|
**+ Standort-Pin auf der Karte**
|
||||||
|
|
||||||
|
**+ Buttons:**
|
||||||
|
- 🗺️ Auf Karte zeigen
|
||||||
|
- 📊 Dashboard öffnen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❓ Häufige Fragen
|
||||||
|
|
||||||
|
### Kann ich sowohl Email als auch Telegram aktivieren?
|
||||||
|
|
||||||
|
Ja! Du kannst beide Kanäle gleichzeitig nutzen:
|
||||||
|
- ☑️ E-Mail Benachrichtigungen
|
||||||
|
- ☑️ Telegram Benachrichtigungen
|
||||||
|
|
||||||
|
### Sehen andere User meine Benachrichtigungen?
|
||||||
|
|
||||||
|
**Nein!** Jede Chat ID ist ein privater 1-zu-1 Chat. Andere User können deine Nachrichten nicht sehen, auch wenn sie den gleichen Bot nutzen.
|
||||||
|
|
||||||
|
### Was ist, wenn die Chat ID falsch ist?
|
||||||
|
|
||||||
|
Dann erhältst du keine Benachrichtigungen. Nutze den "Telegram Test" Button, um die Verbindung zu prüfen.
|
||||||
|
|
||||||
|
### Kann ich Benachrichtigungen pausieren?
|
||||||
|
|
||||||
|
Ja! Deaktiviere einfach "Telegram Benachrichtigungen" in den Einstellungen. Deine Chat ID bleibt gespeichert.
|
||||||
|
|
||||||
|
### Wo finde ich meine Chat ID nochmal?
|
||||||
|
|
||||||
|
Sende `/getid` an **@myidbot** oder `/start` an **@userinfobot** in Telegram.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Troubleshooting
|
||||||
|
|
||||||
|
### "No Telegram chat ID configured"
|
||||||
|
|
||||||
|
→ Du hast noch keine Chat ID eingetragen. Folge Schritt 2 und 3.
|
||||||
|
|
||||||
|
### "Failed to send test message"
|
||||||
|
|
||||||
|
Mögliche Ursachen:
|
||||||
|
1. **Chat ID falsch** → Überprüfe die Nummer bei @userinfobot
|
||||||
|
2. **Bot nicht gestartet** → Sende `/start` an @MeinTracking_bot
|
||||||
|
3. **Bot blockiert** → Entsperre den Bot in Telegram
|
||||||
|
|
||||||
|
### Test-Nachricht kommt nicht an
|
||||||
|
|
||||||
|
1. Überprüfe, ob du den Bot gestartet hast (`/start` an @MeinTracking_bot)
|
||||||
|
2. Prüfe, ob die Chat ID korrekt ist (keine Leerzeichen, keine Buchstaben)
|
||||||
|
- Hole sie neu mit `/getid` an @myidbot
|
||||||
|
3. Stelle sicher, dass du den Bot nicht blockiert hast
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
Bei Problemen wende dich an den Administrator oder überprüfe die Logs im System.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Bot:** [@MeinTracking_bot](https://t.me/MeinTracking_bot)
|
||||||
|
**Letzte Aktualisierung:** 04.12.2025
|
||||||
345
docs/telegram-setup.md
Normal file
345
docs/telegram-setup.md
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
# 🚀 Telegram Integration - Setup Guide
|
||||||
|
|
||||||
|
## ✅ Was wurde implementiert
|
||||||
|
|
||||||
|
### Dateien erstellt:
|
||||||
|
|
||||||
|
1. **Backend Services:**
|
||||||
|
- `lib/telegram-service.ts` - Telegram API Integration
|
||||||
|
- `lib/notification-settings-db.ts` - Datenbank-Layer für Settings
|
||||||
|
- `lib/geofence-notifications.ts` - Erweitert für Multi-Channel (Email + Telegram)
|
||||||
|
|
||||||
|
2. **Database:**
|
||||||
|
- `scripts/init-notification-settings.js` - DB Init Script
|
||||||
|
- Tabelle: `UserNotificationSettings` erstellt
|
||||||
|
|
||||||
|
3. **API Endpoints:**
|
||||||
|
- `GET/PATCH /api/users/[id]/notification-settings` - Settings verwalten
|
||||||
|
- `POST /api/users/[id]/notification-settings/test` - Test-Nachricht
|
||||||
|
|
||||||
|
4. **Admin UI:**
|
||||||
|
- `app/admin/settings/notifications/page.tsx` - Settings Page mit QR Code
|
||||||
|
|
||||||
|
5. **Dokumentation:**
|
||||||
|
- `docs/telegram-anleitung.md` - User-Anleitung
|
||||||
|
- `docs/telegram-setup.md` - Dieses Dokument
|
||||||
|
- `public/telegram-bot-qr.png` - QR Code für Bot
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Installation & Setup
|
||||||
|
|
||||||
|
### 1. Dependencies installiert
|
||||||
|
|
||||||
|
```bash
|
||||||
|
✅ npm install axios
|
||||||
|
✅ npm install qrcode
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Datenbank initialisiert
|
||||||
|
|
||||||
|
```bash
|
||||||
|
✅ node scripts/init-notification-settings.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Tabelle `UserNotificationSettings` wurde erstellt mit:
|
||||||
|
- `user_id` (PK, FK → User.id)
|
||||||
|
- `email_enabled` (0/1)
|
||||||
|
- `telegram_enabled` (0/1)
|
||||||
|
- `telegram_chat_id` (TEXT)
|
||||||
|
- `created_at`, `updated_at`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 Bot Setup
|
||||||
|
|
||||||
|
### Telegram Bot erstellen (für Admin):
|
||||||
|
|
||||||
|
1. Öffne Telegram und suche: **@BotFather**
|
||||||
|
2. Sende: `/newbot`
|
||||||
|
3. Folge den Anweisungen:
|
||||||
|
- Bot Name: `MeinTracking Bot` (oder anders)
|
||||||
|
- Bot Username: `MeinTracking_bot` (muss auf `_bot` enden)
|
||||||
|
4. BotFather gibt dir einen Token: `1234567890:ABCdefGHIjklMNOpqrsTUVwxyz`
|
||||||
|
5. **Wichtig:** Token NIEMALS committen!
|
||||||
|
|
||||||
|
### .env konfigurieren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env oder .env.local
|
||||||
|
TELEGRAM_BOT_TOKEN=1234567890:ABCdefGHIjklMNOpqrsTUVwxyz
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Für User: Bot aktivieren
|
||||||
|
|
||||||
|
**Schritt-für-Schritt siehe:** `docs/telegram-anleitung.md`
|
||||||
|
|
||||||
|
**Kurzversion:**
|
||||||
|
1. Bot starten: [https://t.me/MeinTracking_bot](https://t.me/MeinTracking_bot) oder QR scannen
|
||||||
|
2. Chat ID holen: `@myidbot` → `/getid` (oder `@userinfobot` → `/start`)
|
||||||
|
3. Im Dashboard: Settings → Notifications → Chat ID eintragen
|
||||||
|
4. Test Button drücken
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### 1. QR Code generieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run telegram:qr
|
||||||
|
```
|
||||||
|
|
||||||
|
Erstellt: `public/telegram-bot-qr.png`
|
||||||
|
|
||||||
|
### 2. Datenbank neu initialisieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run db:init:notifications
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder alles zusammen:
|
||||||
|
```bash
|
||||||
|
npm run db:init
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test-Nachricht senden
|
||||||
|
|
||||||
|
**Option A: Über Admin UI**
|
||||||
|
- Navigiere zu: `/admin/settings/notifications`
|
||||||
|
- Telegram aktivieren
|
||||||
|
- Chat ID eintragen
|
||||||
|
- Button "Telegram Test" klicken
|
||||||
|
|
||||||
|
**Option B: Direkt via Script**
|
||||||
|
|
||||||
|
Erstelle `scripts/test-telegram.js`:
|
||||||
|
```javascript
|
||||||
|
const { telegramService } = require('../lib/telegram-service');
|
||||||
|
|
||||||
|
const CHAT_ID = '123456789'; // Deine Chat ID
|
||||||
|
|
||||||
|
async function test() {
|
||||||
|
await telegramService.testConnection(CHAT_ID);
|
||||||
|
}
|
||||||
|
test();
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TELEGRAM_BOT_TOKEN=dein_token node scripts/test-telegram.js
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Sicherheit & Privacy
|
||||||
|
|
||||||
|
### Wie funktioniert die Privacy?
|
||||||
|
|
||||||
|
- **1 Bot für alle User** (in `.env`)
|
||||||
|
- **Jeder User hat eigene Chat ID** (in DB)
|
||||||
|
- **Bot sendet nur an spezifische Chat ID** → Privater 1-zu-1 Chat
|
||||||
|
- **User A sieht NIEMALS Nachrichten von User B**
|
||||||
|
|
||||||
|
### Telegram API Garantie:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
telegram.sendMessage(chatId: "123456789", text: "...")
|
||||||
|
→ Geht NUR an Chat ID 123456789
|
||||||
|
→ Andere Chat IDs können dies NICHT sehen
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vergleich:
|
||||||
|
Wie Email: 1 SMTP-Server (Bot) sendet an verschiedene Adressen (Chat IDs)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Wie es funktioniert
|
||||||
|
|
||||||
|
### Notification Flow:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Device triggers Geofence
|
||||||
|
↓
|
||||||
|
2. geofence-engine.ts erkennt Event
|
||||||
|
↓
|
||||||
|
3. GeofenceEvent in DB erstellt
|
||||||
|
↓
|
||||||
|
4. geofence-notifications.ts aufgerufen
|
||||||
|
↓
|
||||||
|
5. getUserNotificationSettings(owner_id)
|
||||||
|
↓
|
||||||
|
6. Parallel execution:
|
||||||
|
├─→ Email (if enabled)
|
||||||
|
└─→ Telegram (if enabled)
|
||||||
|
↓
|
||||||
|
├─→ Text mit Emoji, Device, Geofence, Zeit
|
||||||
|
├─→ Inline Buttons (Karte, Dashboard)
|
||||||
|
└─→ Location Pin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Channel Strategy:
|
||||||
|
|
||||||
|
- **Promise.allSettled()** → Beide parallel
|
||||||
|
- **Mind. 1 erfolgreich** → Event als "sent" markiert
|
||||||
|
- **Beide fehlgeschlagen** → Event als "failed" markiert
|
||||||
|
- **Robust:** Email-Fehler betrifft nicht Telegram (und umgekehrt)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Admin UI Features
|
||||||
|
|
||||||
|
**Page:** `/admin/settings/notifications`
|
||||||
|
|
||||||
|
**Komponenten:**
|
||||||
|
- ☑️ Email Benachrichtigungen Toggle
|
||||||
|
- ☑️ Telegram Benachrichtigungen Toggle
|
||||||
|
- 📝 Telegram Chat ID Input
|
||||||
|
- 💾 Speichern Button
|
||||||
|
- 🧪 Telegram Test Button
|
||||||
|
- 📱 QR Code Display (400x400px)
|
||||||
|
- 📖 Schritt-für-Schritt Anleitung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ NPM Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Datenbank initialisieren
|
||||||
|
npm run db:init # Alles (inkl. Notifications)
|
||||||
|
npm run db:init:notifications # Nur Notifications Tabelle
|
||||||
|
|
||||||
|
# QR Code generieren
|
||||||
|
npm run telegram:qr # Erstellt public/telegram-bot-qr.png
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
npm run test:geofence # Geofence Event simulieren
|
||||||
|
npm run test:geofence:email # Email Notification testen
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Telegram Message Format
|
||||||
|
|
||||||
|
### Enter Event:
|
||||||
|
```
|
||||||
|
🟢 Geofence BETRETEN
|
||||||
|
|
||||||
|
📱 Device: Mein iPhone
|
||||||
|
📍 Geofence: Zuhause
|
||||||
|
🕐 Zeit: 04.12.25, 14:30
|
||||||
|
📊 Ereignis: Hat Zuhause betreten
|
||||||
|
```
|
||||||
|
|
||||||
|
**+ Location Pin** (Karte mit exakter Position)
|
||||||
|
|
||||||
|
**+ Buttons:**
|
||||||
|
- 🗺️ Auf Karte zeigen → `/map?lat=...&lon=...`
|
||||||
|
- 📊 Dashboard öffnen → `/admin/geofences/events`
|
||||||
|
|
||||||
|
### Exit Event:
|
||||||
|
```
|
||||||
|
🔴 Geofence VERLASSEN
|
||||||
|
|
||||||
|
📱 Device: Mein iPhone
|
||||||
|
📍 Geofence: Zuhause
|
||||||
|
🕐 Zeit: 04.12.25, 18:45
|
||||||
|
📊 Ereignis: Hat Zuhause verlassen
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 Troubleshooting
|
||||||
|
|
||||||
|
### "Cannot find module telegram-service"
|
||||||
|
|
||||||
|
→ TypeScript nicht kompiliert. Lösung:
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder für Development:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Telegram bot token not configured"
|
||||||
|
|
||||||
|
→ `.env` fehlt `TELEGRAM_BOT_TOKEN`
|
||||||
|
|
||||||
|
### "Failed to send test message"
|
||||||
|
|
||||||
|
**Mögliche Ursachen:**
|
||||||
|
1. Bot nicht gestartet (`/start` an @MeinTracking_bot)
|
||||||
|
2. Chat ID falsch
|
||||||
|
3. Bot blockiert
|
||||||
|
4. Token falsch/abgelaufen
|
||||||
|
|
||||||
|
**Debug:**
|
||||||
|
```bash
|
||||||
|
# Check .env
|
||||||
|
cat .env | grep TELEGRAM
|
||||||
|
|
||||||
|
# Check DB
|
||||||
|
sqlite3 data/database.sqlite "SELECT * FROM UserNotificationSettings;"
|
||||||
|
|
||||||
|
# Test Telegram API direkt
|
||||||
|
curl https://api.telegram.org/bot<TOKEN>/getMe
|
||||||
|
```
|
||||||
|
|
||||||
|
### QR Code wird nicht angezeigt
|
||||||
|
|
||||||
|
→ Prüfen ob Datei existiert:
|
||||||
|
```bash
|
||||||
|
ls -la public/telegram-bot-qr.png
|
||||||
|
```
|
||||||
|
|
||||||
|
Falls nicht:
|
||||||
|
```bash
|
||||||
|
npm run telegram:qr
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Required für Telegram
|
||||||
|
TELEGRAM_BOT_TOKEN=<dein_bot_token>
|
||||||
|
|
||||||
|
# Optional (für Map/Dashboard Links)
|
||||||
|
NEXTAUTH_URL=https://deine-domain.de
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Nächste Schritte
|
||||||
|
|
||||||
|
**Für Produktion:**
|
||||||
|
|
||||||
|
1. ✅ `.env.production` mit echtem Bot-Token erstellen
|
||||||
|
2. ✅ Datenbank migrieren: `npm run db:init`
|
||||||
|
3. ✅ QR Code generieren: `npm run telegram:qr`
|
||||||
|
4. ✅ User-Anleitung teilen: `docs/telegram-anleitung.md`
|
||||||
|
5. ✅ Test durchführen mit echtem Device
|
||||||
|
|
||||||
|
**Optional:**
|
||||||
|
- Push Notifications hinzufügen
|
||||||
|
- SMS Integration
|
||||||
|
- Webhook Support
|
||||||
|
- Notification History UI
|
||||||
|
- Per-Geofence Settings (nicht global)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
**Bot:** [@MeinTracking_bot](https://t.me/MeinTracking_bot)
|
||||||
|
|
||||||
|
**Dokumentation:**
|
||||||
|
- User-Anleitung: `docs/telegram-anleitung.md`
|
||||||
|
- API Implementierung: `docs/telegram.md`
|
||||||
|
- Dieses Setup-Guide: `docs/telegram-setup.md`
|
||||||
|
|
||||||
|
**Letzte Aktualisierung:** 04.12.2025
|
||||||
1162
docs/telegram.md
Normal file
1162
docs/telegram.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,19 @@
|
|||||||
/**
|
/**
|
||||||
* Geofence Notification Service
|
* Geofence Notification Service
|
||||||
* Handles sending email notifications for geofence events
|
* Handles sending email and Telegram notifications for geofence events
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { emailService } from './email-service';
|
import { emailService } from './email-service';
|
||||||
|
import { telegramService } from './telegram-service';
|
||||||
import {
|
import {
|
||||||
renderGeofenceEnterEmail,
|
renderGeofenceEnterEmail,
|
||||||
renderGeofenceExitEmail,
|
renderGeofenceExitEmail,
|
||||||
GeofenceEmailData,
|
GeofenceEmailData,
|
||||||
} from './email-renderer';
|
} from './email-renderer';
|
||||||
import { geofenceDb } from './geofence-db';
|
import { geofenceDb } from './geofence-db';
|
||||||
|
import {
|
||||||
|
getUserNotificationSettings,
|
||||||
|
} from './notification-settings-db';
|
||||||
import type { GeofenceEvent } from './types';
|
import type { GeofenceEvent } from './types';
|
||||||
import Database from 'better-sqlite3';
|
import Database from 'better-sqlite3';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -62,6 +66,7 @@ function getUser(userId: string): UserInfo | null {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Send notification for a single geofence event
|
* Send notification for a single geofence event
|
||||||
|
* Supports both Email and Telegram based on user preferences
|
||||||
*/
|
*/
|
||||||
async function sendEventNotification(event: GeofenceEvent): Promise<void> {
|
async function sendEventNotification(event: GeofenceEvent): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -79,13 +84,87 @@ async function sendEventNotification(event: GeofenceEvent): Promise<void> {
|
|||||||
|
|
||||||
// Get owner details
|
// Get owner details
|
||||||
const owner = getUser(geofence.owner_id);
|
const owner = getUser(geofence.owner_id);
|
||||||
if (!owner || !owner.email) {
|
if (!owner) {
|
||||||
console.log(`[GeofenceNotification] No email for owner ${geofence.owner_id}, skipping notification`);
|
throw new Error(`Owner not found: ${geofence.owner_id}`);
|
||||||
geofenceDb.markNotificationSent(event.id!, true); // Mark as "sent" (no email needed)
|
}
|
||||||
|
|
||||||
|
// Get notification preferences
|
||||||
|
const settings = getUserNotificationSettings(owner.id);
|
||||||
|
|
||||||
|
// Prepare notification tasks
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
// Send Email if enabled
|
||||||
|
if (settings.email_enabled && owner.email) {
|
||||||
|
promises.push(
|
||||||
|
sendEmailNotification(event, owner, device, geofence)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send Telegram if enabled
|
||||||
|
if (settings.telegram_enabled && settings.telegram_chat_id) {
|
||||||
|
promises.push(
|
||||||
|
sendTelegramNotification(
|
||||||
|
event,
|
||||||
|
owner,
|
||||||
|
device,
|
||||||
|
geofence,
|
||||||
|
settings.telegram_chat_id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no notification channel enabled, skip
|
||||||
|
if (promises.length === 0) {
|
||||||
|
console.log(
|
||||||
|
`[GeofenceNotification] No notification channels enabled for user ${owner.id}`
|
||||||
|
);
|
||||||
|
geofenceDb.markNotificationSent(event.id!, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare email data
|
// Send notifications in parallel
|
||||||
|
const results = await Promise.allSettled(promises);
|
||||||
|
|
||||||
|
// Check if at least one succeeded
|
||||||
|
const anySuccess = results.some((r) => r.status === 'fulfilled');
|
||||||
|
|
||||||
|
if (anySuccess) {
|
||||||
|
geofenceDb.markNotificationSent(event.id!, true);
|
||||||
|
console.log(
|
||||||
|
`[GeofenceNotification] Sent notification for geofence ${geofence.name}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// All failed
|
||||||
|
const errors = results
|
||||||
|
.filter((r) => r.status === 'rejected')
|
||||||
|
.map((r: any) => r.reason.message)
|
||||||
|
.join('; ');
|
||||||
|
|
||||||
|
geofenceDb.markNotificationSent(event.id!, false, errors);
|
||||||
|
throw new Error(`All notification channels failed: ${errors}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GeofenceNotification] Failed to send notification:', error);
|
||||||
|
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
geofenceDb.markNotificationSent(event.id!, false, errorMessage);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send email notification (extracted from main function)
|
||||||
|
*/
|
||||||
|
async function sendEmailNotification(
|
||||||
|
event: GeofenceEvent,
|
||||||
|
owner: UserInfo,
|
||||||
|
device: DeviceInfo,
|
||||||
|
geofence: GeofenceInfo
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
const emailData: GeofenceEmailData = {
|
const emailData: GeofenceEmailData = {
|
||||||
username: owner.username,
|
username: owner.username,
|
||||||
deviceName: device.name,
|
deviceName: device.name,
|
||||||
@@ -94,11 +173,8 @@ async function sendEventNotification(event: GeofenceEvent): Promise<void> {
|
|||||||
latitude: event.latitude,
|
latitude: event.latitude,
|
||||||
longitude: event.longitude,
|
longitude: event.longitude,
|
||||||
distanceFromCenter: event.distance_from_center || 0,
|
distanceFromCenter: event.distance_from_center || 0,
|
||||||
// Optional: Add map URL later
|
|
||||||
// mapUrl: `${process.env.NEXT_PUBLIC_URL}/map?lat=${event.latitude}&lon=${event.longitude}`
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render and send email
|
|
||||||
let html: string;
|
let html: string;
|
||||||
let subject: string;
|
let subject: string;
|
||||||
|
|
||||||
@@ -110,20 +186,51 @@ async function sendEventNotification(event: GeofenceEvent): Promise<void> {
|
|||||||
subject = `${device.name} hat ${geofence.name} verlassen`;
|
subject = `${device.name} hat ${geofence.name} verlassen`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send via existing email service
|
await emailService['sendEmail'](owner.email!, subject, html);
|
||||||
await emailService['sendEmail'](owner.email, subject, html);
|
|
||||||
|
|
||||||
// Mark notification as sent
|
console.log(
|
||||||
geofenceDb.markNotificationSent(event.id!, true);
|
`[GeofenceNotification] Sent email to ${owner.email}`
|
||||||
|
);
|
||||||
console.log(`[GeofenceNotification] Sent ${event.event_type} notification for geofence ${geofence.name} to ${owner.email}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[GeofenceNotification] Failed to send notification:', error);
|
console.error('[GeofenceNotification] Email error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mark notification as failed
|
/**
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
* Send Telegram notification (new function)
|
||||||
geofenceDb.markNotificationSent(event.id!, false, errorMessage);
|
*/
|
||||||
|
async function sendTelegramNotification(
|
||||||
|
event: GeofenceEvent,
|
||||||
|
owner: UserInfo,
|
||||||
|
device: DeviceInfo,
|
||||||
|
geofence: GeofenceInfo,
|
||||||
|
chatId: string
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
|
||||||
|
|
||||||
|
// Ensure latitude and longitude are numbers
|
||||||
|
const lat = typeof event.latitude === 'number' ? event.latitude : parseFloat(event.latitude);
|
||||||
|
const lon = typeof event.longitude === 'number' ? event.longitude : parseFloat(event.longitude);
|
||||||
|
|
||||||
|
await telegramService.sendGeofenceNotification({
|
||||||
|
chatId,
|
||||||
|
deviceName: device.name,
|
||||||
|
geofenceName: geofence.name,
|
||||||
|
eventType: event.event_type,
|
||||||
|
latitude: lat,
|
||||||
|
longitude: lon,
|
||||||
|
timestamp: event.timestamp,
|
||||||
|
mapUrl: `${baseUrl}/map?lat=${lat}&lon=${lon}`,
|
||||||
|
dashboardUrl: `${baseUrl}/admin/geofences/events`,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[GeofenceNotification] Sent Telegram notification to ${chatId}`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GeofenceNotification] Telegram error:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
157
lib/notification-settings-db.ts
Normal file
157
lib/notification-settings-db.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import Database from 'better-sqlite3';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const dbPath = path.join(process.cwd(), 'data', 'database.sqlite');
|
||||||
|
|
||||||
|
export interface UserNotificationSettings {
|
||||||
|
user_id: string;
|
||||||
|
email_enabled: boolean;
|
||||||
|
telegram_enabled: boolean;
|
||||||
|
telegram_chat_id: string | null;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get notification settings for a user
|
||||||
|
* Returns default settings if not found
|
||||||
|
*/
|
||||||
|
export function getUserNotificationSettings(
|
||||||
|
userId: string
|
||||||
|
): UserNotificationSettings {
|
||||||
|
const db = new Database(dbPath);
|
||||||
|
try {
|
||||||
|
const stmt = db.prepare(`
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
email_enabled,
|
||||||
|
telegram_enabled,
|
||||||
|
telegram_chat_id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM UserNotificationSettings
|
||||||
|
WHERE user_id = ?
|
||||||
|
`);
|
||||||
|
|
||||||
|
const result = stmt.get(userId) as any;
|
||||||
|
|
||||||
|
// Return defaults if no settings found
|
||||||
|
if (!result) {
|
||||||
|
return {
|
||||||
|
user_id: userId,
|
||||||
|
email_enabled: true,
|
||||||
|
telegram_enabled: false,
|
||||||
|
telegram_chat_id: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert SQLite integers to booleans
|
||||||
|
return {
|
||||||
|
user_id: result.user_id,
|
||||||
|
email_enabled: result.email_enabled === 1,
|
||||||
|
telegram_enabled: result.telegram_enabled === 1,
|
||||||
|
telegram_chat_id: result.telegram_chat_id,
|
||||||
|
created_at: result.created_at,
|
||||||
|
updated_at: result.updated_at,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update notification settings for a user
|
||||||
|
*/
|
||||||
|
export function updateUserNotificationSettings(
|
||||||
|
userId: string,
|
||||||
|
settings: Partial<UserNotificationSettings>
|
||||||
|
): UserNotificationSettings {
|
||||||
|
const db = new Database(dbPath);
|
||||||
|
try {
|
||||||
|
// Check if settings exist
|
||||||
|
const existing = db
|
||||||
|
.prepare('SELECT user_id FROM UserNotificationSettings WHERE user_id = ?')
|
||||||
|
.get(userId);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
// Update existing
|
||||||
|
const updates: string[] = [];
|
||||||
|
const values: any[] = [];
|
||||||
|
|
||||||
|
if (settings.email_enabled !== undefined) {
|
||||||
|
updates.push('email_enabled = ?');
|
||||||
|
values.push(settings.email_enabled ? 1 : 0);
|
||||||
|
}
|
||||||
|
if (settings.telegram_enabled !== undefined) {
|
||||||
|
updates.push('telegram_enabled = ?');
|
||||||
|
values.push(settings.telegram_enabled ? 1 : 0);
|
||||||
|
}
|
||||||
|
if (settings.telegram_chat_id !== undefined) {
|
||||||
|
updates.push('telegram_chat_id = ?');
|
||||||
|
values.push(settings.telegram_chat_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
updates.push('updated_at = datetime(\'now\')');
|
||||||
|
values.push(userId);
|
||||||
|
|
||||||
|
const stmt = db.prepare(`
|
||||||
|
UPDATE UserNotificationSettings
|
||||||
|
SET ${updates.join(', ')}
|
||||||
|
WHERE user_id = ?
|
||||||
|
`);
|
||||||
|
|
||||||
|
stmt.run(...values);
|
||||||
|
} else {
|
||||||
|
// Insert new
|
||||||
|
const stmt = db.prepare(`
|
||||||
|
INSERT INTO UserNotificationSettings (
|
||||||
|
user_id,
|
||||||
|
email_enabled,
|
||||||
|
telegram_enabled,
|
||||||
|
telegram_chat_id
|
||||||
|
) VALUES (?, ?, ?, ?)
|
||||||
|
`);
|
||||||
|
|
||||||
|
stmt.run(
|
||||||
|
userId,
|
||||||
|
settings.email_enabled !== undefined ? (settings.email_enabled ? 1 : 0) : 1,
|
||||||
|
settings.telegram_enabled !== undefined ? (settings.telegram_enabled ? 1 : 0) : 0,
|
||||||
|
settings.telegram_chat_id ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getUserNotificationSettings(userId);
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize notification settings table
|
||||||
|
*/
|
||||||
|
export function initNotificationSettingsTable(): void {
|
||||||
|
const db = new Database(dbPath);
|
||||||
|
try {
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS UserNotificationSettings (
|
||||||
|
user_id TEXT PRIMARY KEY,
|
||||||
|
email_enabled INTEGER DEFAULT 1,
|
||||||
|
telegram_enabled INTEGER DEFAULT 0,
|
||||||
|
telegram_chat_id TEXT,
|
||||||
|
created_at TEXT DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT DEFAULT (datetime('now')),
|
||||||
|
|
||||||
|
FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE,
|
||||||
|
CHECK (email_enabled IN (0, 1)),
|
||||||
|
CHECK (telegram_enabled IN (0, 1))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_notification_settings_user
|
||||||
|
ON UserNotificationSettings(user_id);
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('✓ UserNotificationSettings table initialized');
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
170
lib/telegram-service.ts
Normal file
170
lib/telegram-service.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface TelegramMessage {
|
||||||
|
chatId: string;
|
||||||
|
text: string;
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
buttons?: Array<{ text: string; url: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GeofenceNotificationParams {
|
||||||
|
chatId: string;
|
||||||
|
deviceName: string;
|
||||||
|
geofenceName: string;
|
||||||
|
eventType: 'enter' | 'exit';
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
timestamp: string;
|
||||||
|
mapUrl?: string;
|
||||||
|
dashboardUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TelegramService {
|
||||||
|
private botToken: string;
|
||||||
|
private baseUrl: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.botToken = process.env.TELEGRAM_BOT_TOKEN || '';
|
||||||
|
if (!this.botToken) {
|
||||||
|
console.warn('[TelegramService] No TELEGRAM_BOT_TOKEN configured');
|
||||||
|
}
|
||||||
|
this.baseUrl = `https://api.telegram.org/bot${this.botToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Telegram is configured
|
||||||
|
*/
|
||||||
|
isConfigured(): boolean {
|
||||||
|
return !!this.botToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send text message with optional inline buttons
|
||||||
|
*/
|
||||||
|
async sendMessage(params: TelegramMessage): Promise<void> {
|
||||||
|
if (!this.isConfigured()) {
|
||||||
|
throw new Error('Telegram bot token not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { chatId, text, buttons } = params;
|
||||||
|
|
||||||
|
const payload: any = {
|
||||||
|
chat_id: chatId,
|
||||||
|
text: text,
|
||||||
|
parse_mode: 'HTML',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add inline buttons if provided
|
||||||
|
if (buttons && buttons.length > 0) {
|
||||||
|
payload.reply_markup = {
|
||||||
|
inline_keyboard: [
|
||||||
|
buttons.map(btn => ({
|
||||||
|
text: btn.text,
|
||||||
|
url: btn.url,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.post(`${this.baseUrl}/sendMessage`, payload);
|
||||||
|
|
||||||
|
if (!response.data.ok) {
|
||||||
|
throw new Error(`Telegram API error: ${response.data.description}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send location pin on map
|
||||||
|
*/
|
||||||
|
async sendLocation(
|
||||||
|
chatId: string,
|
||||||
|
latitude: number,
|
||||||
|
longitude: number
|
||||||
|
): Promise<void> {
|
||||||
|
if (!this.isConfigured()) {
|
||||||
|
throw new Error('Telegram bot token not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.post(`${this.baseUrl}/sendLocation`, {
|
||||||
|
chat_id: chatId,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.data.ok) {
|
||||||
|
throw new Error(`Telegram API error: ${response.data.description}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send geofence notification (complete with text + location + buttons)
|
||||||
|
*/
|
||||||
|
async sendGeofenceNotification(
|
||||||
|
params: GeofenceNotificationParams
|
||||||
|
): Promise<void> {
|
||||||
|
const {
|
||||||
|
chatId,
|
||||||
|
deviceName,
|
||||||
|
geofenceName,
|
||||||
|
eventType,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
timestamp,
|
||||||
|
mapUrl,
|
||||||
|
dashboardUrl,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
// Format message
|
||||||
|
const emoji = eventType === 'enter' ? '🟢' : '🔴';
|
||||||
|
const action = eventType === 'enter' ? 'BETRETEN' : 'VERLASSEN';
|
||||||
|
const verb = eventType === 'enter' ? 'betreten' : 'verlassen';
|
||||||
|
|
||||||
|
const formattedDate = new Date(timestamp).toLocaleString('de-DE', {
|
||||||
|
dateStyle: 'short',
|
||||||
|
timeStyle: 'short',
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = `
|
||||||
|
${emoji} <b>Geofence ${action}</b>
|
||||||
|
|
||||||
|
📱 <b>Device:</b> ${deviceName}
|
||||||
|
📍 <b>Geofence:</b> ${geofenceName}
|
||||||
|
🕐 <b>Zeit:</b> ${formattedDate}
|
||||||
|
📊 <b>Ereignis:</b> Hat ${geofenceName} ${verb}
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
// Prepare inline buttons
|
||||||
|
const buttons = [];
|
||||||
|
if (mapUrl) {
|
||||||
|
buttons.push({ text: '🗺️ Auf Karte zeigen', url: mapUrl });
|
||||||
|
}
|
||||||
|
if (dashboardUrl) {
|
||||||
|
buttons.push({ text: '📊 Dashboard öffnen', url: dashboardUrl });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send text message with buttons
|
||||||
|
await this.sendMessage({ chatId, text, buttons });
|
||||||
|
|
||||||
|
// Send location pin
|
||||||
|
await this.sendLocation(chatId, latitude, longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test connection by sending a simple message
|
||||||
|
*/
|
||||||
|
async testConnection(chatId: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.sendMessage({
|
||||||
|
chatId,
|
||||||
|
text: '✅ <b>Telegram Connection Test</b>\n\nDie Verbindung funktioniert!',
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[TelegramService] Test failed:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const telegramService = new TelegramService();
|
||||||
600
package-lock.json
generated
600
package-lock.json
generated
@@ -16,6 +16,7 @@
|
|||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.4",
|
"@types/react": "^19.2.4",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"axios": "^1.13.2",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"better-sqlite3": "^12.4.1",
|
"better-sqlite3": "^12.4.1",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
"next": "^16.0.7",
|
"next": "^16.0.7",
|
||||||
"next-auth": "^5.0.0-beta.30",
|
"next-auth": "^5.0.0-beta.30",
|
||||||
"nodemailer": "^7.0.10",
|
"nodemailer": "^7.0.10",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
"react-email": "^4.3.2",
|
"react-email": "^4.3.2",
|
||||||
@@ -3416,6 +3418,12 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.22",
|
"version": "10.4.22",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
|
||||||
@@ -3454,6 +3462,17 @@
|
|||||||
"postcss": "^8.1.0"
|
"postcss": "^8.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.13.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||||
|
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.4",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@@ -3619,6 +3638,28 @@
|
|||||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/camelcase": {
|
||||||
|
"version": "5.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||||
|
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001754",
|
"version": "1.0.30001754",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
|
||||||
@@ -3714,6 +3755,87 @@
|
|||||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.0",
|
||||||
|
"wrap-ansi": "^6.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cliui/node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cliui/node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cliui/node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/cliui/node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cliui/node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cliui/node_modules/wrap-ansi": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -3732,6 +3854,18 @@
|
|||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "13.1.0",
|
"version": "13.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
|
||||||
@@ -3848,6 +3982,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decamelize": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/decompress-response": {
|
"node_modules/decompress-response": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
@@ -3881,6 +4024,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
@@ -3890,6 +4042,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dijkstrajs": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dom-serializer": {
|
"node_modules/dom-serializer": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
@@ -3945,6 +4103,20 @@
|
|||||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eastasianwidth": {
|
"node_modules/eastasianwidth": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
@@ -4044,6 +4216,51 @@
|
|||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||||
@@ -4172,6 +4389,39 @@
|
|||||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/find-up": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"locate-path": "^5.0.0",
|
||||||
|
"path-exists": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/foreground-child": {
|
"node_modules/foreground-child": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
@@ -4188,6 +4438,43 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||||
|
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data/node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data/node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fraction.js": {
|
"node_modules/fraction.js": {
|
||||||
"version": "5.3.4",
|
"version": "5.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||||
@@ -4208,6 +4495,24 @@
|
|||||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-caller-file": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-east-asian-width": {
|
"node_modules/get-east-asian-width": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
|
||||||
@@ -4220,6 +4525,43 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/github-from-package": {
|
"node_modules/github-from-package": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||||
@@ -4249,12 +4591,63 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/help-me": {
|
"node_modules/help-me": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
|
||||||
@@ -4722,6 +5115,18 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/locate-path": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-locate": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/log-symbols": {
|
"node_modules/log-symbols": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
|
||||||
@@ -4768,6 +5173,15 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mime-db": {
|
"node_modules/mime-db": {
|
||||||
"version": "1.54.0",
|
"version": "1.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||||
@@ -5352,6 +5766,42 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/p-limit": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-try": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-locate": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-limit": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-try": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/package-json-from-dist": {
|
"node_modules/package-json-from-dist": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||||
@@ -5371,6 +5821,15 @@
|
|||||||
"url": "https://ko-fi.com/killymxi"
|
"url": "https://ko-fi.com/killymxi"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-exists": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-key": {
|
"node_modules/path-key": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||||
@@ -5428,6 +5887,15 @@
|
|||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pngjs": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
@@ -5560,6 +6028,12 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pump": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||||
@@ -5570,6 +6044,23 @@
|
|||||||
"once": "^1.3.1"
|
"once": "^1.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dijkstrajs": "^1.0.1",
|
||||||
|
"pngjs": "^5.0.0",
|
||||||
|
"yargs": "^15.3.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"qrcode": "bin/qrcode"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rc": {
|
"node_modules/rc": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||||
@@ -5695,6 +6186,21 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-directory": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/require-main-filename": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/restore-cursor": {
|
"node_modules/restore-cursor": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
|
||||||
@@ -5767,6 +6273,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-blocking": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/sharp": {
|
"node_modules/sharp": {
|
||||||
"version": "0.34.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||||
@@ -6385,6 +6897,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/which-module": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/worker-factory": {
|
"node_modules/worker-factory": {
|
||||||
"version": "7.0.46",
|
"version": "7.0.46",
|
||||||
"resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.46.tgz",
|
"resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.46.tgz",
|
||||||
@@ -6550,6 +7068,88 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "15.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||||
|
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^6.0.0",
|
||||||
|
"decamelize": "^1.2.0",
|
||||||
|
"find-up": "^4.1.0",
|
||||||
|
"get-caller-file": "^2.0.1",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"require-main-filename": "^2.0.0",
|
||||||
|
"set-blocking": "^2.0.0",
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"which-module": "^2.0.0",
|
||||||
|
"y18n": "^4.0.0",
|
||||||
|
"yargs-parser": "^18.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "18.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||||
|
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"camelcase": "^5.0.0",
|
||||||
|
"decamelize": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs/node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs/node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/yargs/node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs/node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yoctocolors": {
|
"node_modules/yoctocolors": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
|
||||||
|
|||||||
@@ -8,10 +8,11 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"db:init": "node scripts/init-database.js && node scripts/init-locations-db.js && node scripts/init-geofence-db.js",
|
"db:init": "node scripts/init-database.js && node scripts/init-locations-db.js && node scripts/init-geofence-db.js && node scripts/init-notification-settings.js",
|
||||||
"db:init:app": "node scripts/init-database.js",
|
"db:init:app": "node scripts/init-database.js",
|
||||||
"db:init:locations": "node scripts/init-locations-db.js",
|
"db:init:locations": "node scripts/init-locations-db.js",
|
||||||
"db:init:geofence": "node scripts/init-geofence-db.js",
|
"db:init:geofence": "node scripts/init-geofence-db.js",
|
||||||
|
"db:init:notifications": "node scripts/init-notification-settings.js",
|
||||||
"db:cleanup": "node scripts/cleanup-old-locations.js",
|
"db:cleanup": "node scripts/cleanup-old-locations.js",
|
||||||
"db:cleanup:7d": "node scripts/cleanup-old-locations.js 168",
|
"db:cleanup:7d": "node scripts/cleanup-old-locations.js 168",
|
||||||
"db:cleanup:30d": "node scripts/cleanup-old-locations.js 720",
|
"db:cleanup:30d": "node scripts/cleanup-old-locations.js 720",
|
||||||
@@ -21,7 +22,8 @@
|
|||||||
"test:geofence:mqtt": "node scripts/test-mqtt-geofence.js",
|
"test:geofence:mqtt": "node scripts/test-mqtt-geofence.js",
|
||||||
"docs:check": "node scripts/check-docs.js",
|
"docs:check": "node scripts/check-docs.js",
|
||||||
"docs:sync": "node scripts/sync-docs.js",
|
"docs:sync": "node scripts/sync-docs.js",
|
||||||
"email:dev": "email dev"
|
"email:dev": "email dev",
|
||||||
|
"telegram:qr": "node scripts/generate-telegram-qr.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -34,6 +36,7 @@
|
|||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.4",
|
"@types/react": "^19.2.4",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"axios": "^1.13.2",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"better-sqlite3": "^12.4.1",
|
"better-sqlite3": "^12.4.1",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
@@ -41,6 +44,7 @@
|
|||||||
"next": "^16.0.7",
|
"next": "^16.0.7",
|
||||||
"next-auth": "^5.0.0-beta.30",
|
"next-auth": "^5.0.0-beta.30",
|
||||||
"nodemailer": "^7.0.10",
|
"nodemailer": "^7.0.10",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
"react-email": "^4.3.2",
|
"react-email": "^4.3.2",
|
||||||
|
|||||||
BIN
public/telegram-bot-qr.png
Normal file
BIN
public/telegram-bot-qr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
40
scripts/generate-telegram-qr.js
Executable file
40
scripts/generate-telegram-qr.js
Executable file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Generate QR Code for Telegram Bot
|
||||||
|
*/
|
||||||
|
|
||||||
|
const QRCode = require('qrcode');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const BOT_URL = 'https://t.me/MeinTracking_bot';
|
||||||
|
const OUTPUT_DIR = path.join(process.cwd(), 'public');
|
||||||
|
const OUTPUT_FILE = path.join(OUTPUT_DIR, 'telegram-bot-qr.png');
|
||||||
|
|
||||||
|
async function generateQR() {
|
||||||
|
try {
|
||||||
|
// Ensure public directory exists
|
||||||
|
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||||
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate QR Code
|
||||||
|
await QRCode.toFile(OUTPUT_FILE, BOT_URL, {
|
||||||
|
width: 400,
|
||||||
|
margin: 2,
|
||||||
|
color: {
|
||||||
|
dark: '#000000',
|
||||||
|
light: '#FFFFFF'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ QR Code generated: ${OUTPUT_FILE}`);
|
||||||
|
console.log(`📱 Bot URL: ${BOT_URL}`);
|
||||||
|
console.log(`🖼️ QR Code size: 400x400px`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error generating QR Code:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateQR();
|
||||||
38
scripts/init-notification-settings.js
Executable file
38
scripts/init-notification-settings.js
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Initialize UserNotificationSettings table
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Database = require('better-sqlite3');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const dbPath = path.join(process.cwd(), 'data', 'database.sqlite');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = new Database(dbPath);
|
||||||
|
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS UserNotificationSettings (
|
||||||
|
user_id TEXT PRIMARY KEY,
|
||||||
|
email_enabled INTEGER DEFAULT 1,
|
||||||
|
telegram_enabled INTEGER DEFAULT 0,
|
||||||
|
telegram_chat_id TEXT,
|
||||||
|
created_at TEXT DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT DEFAULT (datetime('now')),
|
||||||
|
|
||||||
|
FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE,
|
||||||
|
CHECK (email_enabled IN (0, 1)),
|
||||||
|
CHECK (telegram_enabled IN (0, 1))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_notification_settings_user
|
||||||
|
ON UserNotificationSettings(user_id);
|
||||||
|
`);
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
|
||||||
|
console.log('✅ Notification settings table initialized!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
56
scripts/test-telegram-token.js
Executable file
56
scripts/test-telegram-token.js
Executable file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Test Telegram Bot Token
|
||||||
|
*/
|
||||||
|
|
||||||
|
const axios = require('axios');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Read .env manually
|
||||||
|
const envPath = path.join(process.cwd(), '.env');
|
||||||
|
const envContent = fs.readFileSync(envPath, 'utf-8');
|
||||||
|
const BOT_TOKEN = envContent
|
||||||
|
.split('\n')
|
||||||
|
.find(line => line.startsWith('TELEGRAM_BOT_TOKEN='))
|
||||||
|
?.split('=')[1]
|
||||||
|
?.trim();
|
||||||
|
|
||||||
|
async function testToken() {
|
||||||
|
if (!BOT_TOKEN) {
|
||||||
|
console.error('❌ TELEGRAM_BOT_TOKEN not found in .env');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🧪 Testing Telegram Bot Token...\n');
|
||||||
|
console.log(`Token: ${BOT_TOKEN.substring(0, 15)}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`https://api.telegram.org/bot${BOT_TOKEN}/getMe`);
|
||||||
|
|
||||||
|
if (response.data.ok) {
|
||||||
|
const bot = response.data.result;
|
||||||
|
console.log('\n✅ Bot Token is VALID!\n');
|
||||||
|
console.log('📱 Bot Information:');
|
||||||
|
console.log(` ID: ${bot.id}`);
|
||||||
|
console.log(` Name: ${bot.first_name}`);
|
||||||
|
console.log(` Username: @${bot.username}`);
|
||||||
|
console.log(` Can Join Groups: ${bot.can_join_groups}`);
|
||||||
|
console.log(` Can Read Messages: ${bot.can_read_all_group_messages}`);
|
||||||
|
console.log('\n🔗 Bot Link: https://t.me/' + bot.username);
|
||||||
|
} else {
|
||||||
|
console.error('❌ Invalid response:', response.data);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ Token test FAILED!');
|
||||||
|
if (error.response) {
|
||||||
|
console.error('Error:', error.response.data.description);
|
||||||
|
} else {
|
||||||
|
console.error('Error:', error.message);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testToken();
|
||||||
Reference in New Issue
Block a user