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:
2025-12-04 14:54:19 +00:00
parent 17aaf130a8
commit 0d1dbeafda
18 changed files with 3200 additions and 21 deletions

View File

@@ -36,6 +36,7 @@ RUN npm ci --omit=dev
# Kopiere Build Artifacts
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/data ./data
COPY --from=builder /app/public ./public
# Kopiere App Code (benötigt für instrumentation.ts, lib/, etc.)
COPY --from=builder /app/instrumentation.ts ./

View File

@@ -1093,7 +1093,6 @@ npm run email:dev # Email Template Dev Server
# Linting
npm run lint # ESLint ausführen
```
---
## 📚 Automatische Dokumentations-Synchronisation

View 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>
);
}

View 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 }
);
}
}

View 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 }
);
}
}

View File

@@ -30,6 +30,8 @@ services:
- SMTP_FROM_EMAIL=${SMTP_FROM_EMAIL}
- SMTP_FROM_NAME=${SMTP_FROM_NAME:-Location Tracker}
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
# Telegram Configuration
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
volumes:
- ./data:/app/data
- ./public:/app/public # Mount public directory for static files

163
docs/telegram-anleitung.md Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,19 @@
/**
* 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 { telegramService } from './telegram-service';
import {
renderGeofenceEnterEmail,
renderGeofenceExitEmail,
GeofenceEmailData,
} from './email-renderer';
import { geofenceDb } from './geofence-db';
import {
getUserNotificationSettings,
} from './notification-settings-db';
import type { GeofenceEvent } from './types';
import Database from 'better-sqlite3';
import path from 'path';
@@ -62,6 +66,7 @@ function getUser(userId: string): UserInfo | null {
/**
* Send notification for a single geofence event
* Supports both Email and Telegram based on user preferences
*/
async function sendEventNotification(event: GeofenceEvent): Promise<void> {
try {
@@ -79,13 +84,87 @@ async function sendEventNotification(event: GeofenceEvent): Promise<void> {
// Get owner details
const owner = getUser(geofence.owner_id);
if (!owner || !owner.email) {
console.log(`[GeofenceNotification] No email for owner ${geofence.owner_id}, skipping notification`);
geofenceDb.markNotificationSent(event.id!, true); // Mark as "sent" (no email needed)
if (!owner) {
throw new Error(`Owner not found: ${geofence.owner_id}`);
}
// 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;
}
// 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 = {
username: owner.username,
deviceName: device.name,
@@ -94,11 +173,8 @@ async function sendEventNotification(event: GeofenceEvent): Promise<void> {
latitude: event.latitude,
longitude: event.longitude,
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 subject: string;
@@ -110,20 +186,51 @@ async function sendEventNotification(event: GeofenceEvent): Promise<void> {
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
geofenceDb.markNotificationSent(event.id!, true);
console.log(`[GeofenceNotification] Sent ${event.event_type} notification for geofence ${geofence.name} to ${owner.email}`);
console.log(
`[GeofenceNotification] Sent email to ${owner.email}`
);
} 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';
geofenceDb.markNotificationSent(event.id!, false, errorMessage);
/**
* Send Telegram notification (new function)
*/
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;
}
}

View 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
View 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
View File

@@ -16,6 +16,7 @@
"@types/node": "^24.10.1",
"@types/react": "^19.2.4",
"@types/react-dom": "^19.2.3",
"axios": "^1.13.2",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^12.4.1",
"leaflet": "^1.9.4",
@@ -23,6 +24,7 @@
"next": "^16.0.7",
"next-auth": "^5.0.0-beta.30",
"nodemailer": "^7.0.10",
"qrcode": "^1.5.4",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-email": "^4.3.2",
@@ -3416,6 +3418,12 @@
"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": {
"version": "10.4.22",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
@@ -3454,6 +3462,17 @@
"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": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -3619,6 +3638,28 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"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": {
"version": "1.0.30001754",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
@@ -3714,6 +3755,87 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3732,6 +3854,18 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"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": {
"version": "13.1.0",
"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": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@@ -3881,6 +4024,15 @@
"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": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -3890,6 +4042,12 @@
"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": {
"version": "2.0.0",
"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"
}
},
"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": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -4044,6 +4216,51 @@
"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": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
@@ -4172,6 +4389,39 @@
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"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": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -4188,6 +4438,43 @@
"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": {
"version": "5.3.4",
"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==",
"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": {
"version": "1.4.0",
"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"
}
},
"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": {
"version": "0.0.0",
"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"
}
},
"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": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"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": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
@@ -4722,6 +5115,18 @@
"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": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
@@ -4768,6 +5173,15 @@
"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": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
@@ -5352,6 +5766,42 @@
"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": {
"version": "1.0.1",
"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"
}
},
"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": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -5428,6 +5887,15 @@
"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": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -5560,6 +6028,12 @@
"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": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
@@ -5570,6 +6044,23 @@
"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": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@@ -5695,6 +6186,21 @@
"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": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
@@ -5767,6 +6273,12 @@
"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": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -6385,6 +6897,12 @@
"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": {
"version": "7.0.46",
"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": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",

View File

@@ -8,10 +8,11 @@
"build": "next build",
"start": "next start",
"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:locations": "node scripts/init-locations-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:7d": "node scripts/cleanup-old-locations.js 168",
"db:cleanup:30d": "node scripts/cleanup-old-locations.js 720",
@@ -21,7 +22,8 @@
"test:geofence:mqtt": "node scripts/test-mqtt-geofence.js",
"docs:check": "node scripts/check-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": [],
"author": "",
@@ -34,6 +36,7 @@
"@types/node": "^24.10.1",
"@types/react": "^19.2.4",
"@types/react-dom": "^19.2.3",
"axios": "^1.13.2",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^12.4.1",
"leaflet": "^1.9.4",
@@ -41,6 +44,7 @@
"next": "^16.0.7",
"next-auth": "^5.0.0-beta.30",
"nodemailer": "^7.0.10",
"qrcode": "^1.5.4",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-email": "^4.3.2",

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
View 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();

View 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
View 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();