Commit 85cc2200 authored by Hendrik Garske's avatar Hendrik Garske

feat: Vollständige WireGuard Easy Funktionalität implementiert

parent 01c6d240
......@@ -14,8 +14,12 @@ WG Easy ist eine moderne Webanwendung zur Verwaltung von WireGuard VPN-Servern u
## ✨ Features
- 🔐 **Client-Verwaltung**: Erstellen und verwalten Sie VPN-Clients
- 🔐 **Client-Verwaltung**: Erstellen, löschen und verwalten Sie VPN-Clients
- 📊 **Dashboard**: Übersicht über aktive Verbindungen und Traffic
- 📥 **Konfigurations-Download**: WireGuard .conf Dateien herunterladen
- 📱 **QR-Code**: QR-Codes für einfache Client-Einrichtung
- 🔌 **Client-Aktivierung**: Clients aktivieren/deaktivieren
- 📈 **Live-Statistiken**: Echtzeit-Traffic-Statistiken
- 🎨 **Modernes Design**: CoreX Design System
- 🌓 **Dark Mode**: Automatischer Dark/Light Mode
- 📱 **Responsive**: Optimiert für alle Geräte
......@@ -37,6 +41,7 @@ WG Easy ist eine moderne Webanwendung zur Verwaltung von WireGuard VPN-Servern u
- **npm** oder **yarn** oder **pnpm**
- **Git** (für Clone)
- **Docker & Docker Compose** (optional)
- **wg-easy Backend**: WireGuard Easy Docker Container (siehe Setup unten)
## 🚀 Installation
......@@ -53,7 +58,38 @@ cd wg-easy
npm install
```
### 3. Development Server starten
### 3. WireGuard Easy Backend starten
```bash
docker run -d \
--name=wg-easy \
-e WG_HOST=YOUR_SERVER_IP \
-e PASSWORD=YOUR_PASSWORD \
-v ~/.wg-easy:/etc/wireguard \
-p 51820:51820/udp \
-p 51821:51821/tcp \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
--sysctl="net.ipv4.conf.all.src_valid_mark=1" \
--sysctl="net.ipv4.ip_forward=1" \
--restart unless-stopped \
weejewel/wg-easy
```
### 4. Umgebungsvariablen konfigurieren (Optional)
Erstellen Sie eine `.env.local` Datei:
```env
WG_API_URL=http://localhost:51821
WG_PASSWORD=YOUR_PASSWORD
```
**Standardwerte:**
- `WG_API_URL`: `http://localhost:51821`
- `WG_PASSWORD`: leer (falls kein Passwort gesetzt)
### 5. Development Server starten
```bash
npm run dev
......
import { NextRequest, NextResponse } from "next/server"
import { wgApi } from "@/lib/wg-api"
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const config = await wgApi.getClientConfig(id)
return new NextResponse(config, {
headers: {
'Content-Type': 'text/plain',
'Content-Disposition': `attachment; filename="wg-${id}.conf"`,
},
})
} catch (error) {
console.error("Error fetching client config:", error)
const errorMessage = error instanceof Error ? error.message : "Failed to fetch config"
return NextResponse.json(
{ error: errorMessage },
{ status: 500 }
)
}
}
import { NextRequest, NextResponse } from "next/server"
import { wgApi } from "@/lib/wg-api"
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const qrCode = await wgApi.getClientQR(id)
return new NextResponse(qrCode, {
headers: {
'Content-Type': 'image/svg+xml',
},
})
} catch (error) {
console.error("Error fetching QR code:", error)
const errorMessage = error instanceof Error ? error.message : "Failed to fetch QR code"
return NextResponse.json(
{ error: errorMessage },
{ status: 500 }
)
}
}
import { NextRequest, NextResponse } from "next/server"
import { wgApi } from "@/lib/wg-api"
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
await wgApi.deleteClient(id)
return NextResponse.json({ success: true })
} catch (error) {
console.error("Error deleting client:", error)
const errorMessage = error instanceof Error ? error.message : "Failed to delete client"
return NextResponse.json(
{ error: errorMessage },
{ status: 500 }
)
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const body = await request.json()
const { enabled } = body
if (enabled === true) {
await wgApi.enableClient(id)
} else if (enabled === false) {
await wgApi.disableClient(id)
} else {
return NextResponse.json(
{ error: "enabled field is required (true/false)" },
{ status: 400 }
)
}
return NextResponse.json({ success: true })
} catch (error) {
console.error("Error updating client:", error)
const errorMessage = error instanceof Error ? error.message : "Failed to update client"
return NextResponse.json(
{ error: errorMessage },
{ status: 500 }
)
}
}
import { NextRequest, NextResponse } from "next/server"
import { wgApi } from "@/lib/wg-api"
export async function GET() {
try {
const clients = await wgApi.getClients()
return NextResponse.json(clients)
} catch (error) {
console.error("Error fetching clients:", error)
const errorMessage = error instanceof Error ? error.message : "Failed to fetch clients"
return NextResponse.json(
{ error: errorMessage },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { name, allowedIPs } = body
if (!name || typeof name !== 'string' || name.trim() === '') {
return NextResponse.json(
{ error: "Client name is required" },
{ status: 400 }
)
}
const client = await wgApi.createClient({
name: name.trim(),
allowedIPs: allowedIPs || undefined,
})
return NextResponse.json(client, { status: 201 })
} catch (error) {
console.error("Error creating client:", error)
const errorMessage = error instanceof Error ? error.message : "Failed to create client"
return NextResponse.json(
{ error: errorMessage },
{ status: 500 }
)
}
}
import { NextResponse } from "next/server"
import { wgApi } from "@/lib/wg-api"
export async function GET() {
try {
const stats = await wgApi.getStats()
return NextResponse.json(stats)
} catch (error) {
console.error("Error fetching stats:", error)
const errorMessage = error instanceof Error ? error.message : "Failed to fetch stats"
return NextResponse.json(
{ error: errorMessage },
{ status: 500 }
)
}
}
"use client"
import { useState, useEffect } from "react"
import { Logo } from "@/components/Logo"
import { ThemeToggle } from "@/components/theme-toggle"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Shield, Users, Network, Download } from "lucide-react"
import Link from "next/link"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Shield, Users, Network, Download, Plus, Trash2, Power, PowerOff, QrCode, FileDown } from "lucide-react"
interface Client {
id: string
name: string
enabled: boolean
address: string
publicKey: string
createdAt: string
updatedAt: string
latestHandshakeAt: string
transferRx: number
transferTx: number
persistentKeepalive: string
}
interface Server {
address: string
listenPort: number
publicKey: string
endpoint: string
dns: string
allowedIPs: string
createdAt: string
}
export default function Dashboard() {
const [clients, setClients] = useState<Client[]>([])
const [server, setServer] = useState<Server | null>(null)
const [loading, setLoading] = useState(true)
const [showAddClient, setShowAddClient] = useState(false)
const [newClientName, setNewClientName] = useState("")
const [isCreating, setIsCreating] = useState(false)
useEffect(() => {
fetchStats()
// Refresh every 10 seconds
const interval = setInterval(fetchStats, 10000)
return () => clearInterval(interval)
}, [])
const fetchStats = async () => {
try {
const response = await fetch('/api/stats')
if (response.ok) {
const data = await response.json()
setClients(data.clients || [])
setServer(data.server || null)
}
} catch (error) {
console.error("Error fetching stats:", error)
} finally {
setLoading(false)
}
}
const handleCreateClient = async () => {
if (!newClientName.trim()) return
setIsCreating(true)
try {
const response = await fetch('/api/clients', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newClientName.trim() }),
})
if (response.ok) {
const client = await response.json()
setNewClientName("")
setShowAddClient(false)
await fetchStats()
// Show success message or download config
alert(`Client "${client.name}" erstellt!`)
} else {
const error = await response.json()
alert(`Fehler: ${error.error}`)
}
} catch (error) {
console.error("Error creating client:", error)
alert("Fehler beim Erstellen des Clients")
} finally {
setIsCreating(false)
}
}
const handleDeleteClient = async (id: string, name: string) => {
if (!confirm(`Möchten Sie den Client "${name}" wirklich löschen?`)) return
try {
const response = await fetch(`/api/clients/${id}`, {
method: 'DELETE',
})
if (response.ok) {
await fetchStats()
} else {
const error = await response.json()
alert(`Fehler: ${error.error}`)
}
} catch (error) {
console.error("Error deleting client:", error)
alert("Fehler beim Löschen des Clients")
}
}
const handleToggleClient = async (id: string, enabled: boolean) => {
try {
const response = await fetch(`/api/clients/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: !enabled }),
})
if (response.ok) {
await fetchStats()
} else {
const error = await response.json()
alert(`Fehler: ${error.error}`)
}
} catch (error) {
console.error("Error toggling client:", error)
alert("Fehler beim Ändern des Client-Status")
}
}
const handleDownloadConfig = (id: string, name: string) => {
window.open(`/api/clients/${id}/config`, '_blank')
}
const handleShowQR = (id: string) => {
window.open(`/api/clients/${id}/qr`, '_blank')
}
const formatBytes = (bytes: number) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
}
const formatDate = (dateString: string) => {
if (!dateString) return 'Nie'
const date = new Date(dateString)
return date.toLocaleString('de-DE')
}
const activeClients = clients.filter(c => c.enabled).length
const totalRx = clients.reduce((sum, c) => sum + c.transferRx, 0)
const totalTx = clients.reduce((sum, c) => sum + c.transferTx, 0)
const totalTraffic = totalRx + totalTx
return (
<div className="min-h-screen bg-background">
{/* Header */}
......@@ -23,12 +175,53 @@ export default function Dashboard() {
{/* Main Content */}
<main className="container mx-auto px-4 py-8">
<div className="mb-8">
<div className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">WireGuard VPN Management</h1>
<p className="text-muted-foreground mt-2">
Verwalten Sie Ihre WireGuard VPN-Konfigurationen
</p>
</div>
<Button onClick={() => setShowAddClient(!showAddClient)}>
<Plus className="h-4 w-4 mr-2" />
Neuer Client
</Button>
</div>
{/* Add Client Form */}
{showAddClient && (
<Card className="mb-8">
<CardHeader>
<CardTitle>Neuer Client hinzufügen</CardTitle>
<CardDescription>Erstellen Sie einen neuen WireGuard VPN Client</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="clientName">Client Name</Label>
<Input
id="clientName"
placeholder="z.B. Laptop Max Mustermann"
value={newClientName}
onChange={(e) => setNewClientName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleCreateClient()
}}
/>
</div>
<div className="flex gap-2">
<Button onClick={handleCreateClient} disabled={isCreating || !newClientName.trim()}>
{isCreating ? 'Wird erstellt...' : 'Client erstellen'}
</Button>
<Button variant="outline" onClick={() => {
setShowAddClient(false)
setNewClientName("")
}}>
Abbrechen
</Button>
</div>
</CardContent>
</Card>
)}
{/* Stats Cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-8">
......@@ -38,7 +231,7 @@ export default function Dashboard() {
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">0</div>
<div className="text-2xl font-bold">{activeClients} / {clients.length}</div>
<p className="text-xs text-muted-foreground">Verbundene Clients</p>
</CardContent>
</Card>
......@@ -49,8 +242,8 @@ export default function Dashboard() {
<Shield className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">Online</div>
<p className="text-xs text-muted-foreground">Server läuft</p>
<div className="text-2xl font-bold">{server ? 'Online' : 'Offline'}</div>
<p className="text-xs text-muted-foreground">{server ? `Port: ${server.listenPort}` : 'Server nicht erreichbar'}</p>
</CardContent>
</Card>
......@@ -60,43 +253,103 @@ export default function Dashboard() {
<Network className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">0 MB</div>
<p className="text-xs text-muted-foreground">Upload/Download</p>
<div className="text-2xl font-bold">{formatBytes(totalTraffic)}</div>
<p className="text-xs text-muted-foreground">{formatBytes(totalTx)} / ↓ {formatBytes(totalRx)}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Downloads</CardTitle>
<CardTitle className="text-sm font-medium">Clients gesamt</CardTitle>
<Download className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">0</div>
<div className="text-2xl font-bold">{clients.length}</div>
<p className="text-xs text-muted-foreground">Konfigurationen</p>
</CardContent>
</Card>
</div>
{/* Quick Actions */}
{/* Clients List */}
<Card>
<CardHeader>
<CardTitle>Schnellzugriff</CardTitle>
<CardDescription>Häufig verwendete Funktionen</CardDescription>
<CardTitle>VPN Clients</CardTitle>
<CardDescription>Verwalten Sie Ihre WireGuard VPN Clients</CardDescription>
</CardHeader>
<CardContent className="flex flex-wrap gap-4">
<Button>
Neuer Client hinzufügen
<CardContent>
{loading ? (
<div className="text-center py-8 text-muted-foreground">Lade Daten...</div>
) : clients.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
Noch keine Clients vorhanden. Erstellen Sie einen neuen Client.
</div>
) : (
<div className="space-y-4">
{clients.map((client) => (
<div
key={client.id}
className="flex items-center justify-between p-4 border rounded-lg hover:bg-accent/50 transition-colors"
>
<div className="flex-1">
<div className="flex items-center gap-3">
<div className={`w-3 h-3 rounded-full ${client.enabled ? 'bg-green-500' : 'bg-gray-400'}`} />
<div>
<h3 className="font-semibold">{client.name}</h3>
<div className="text-sm text-muted-foreground space-y-1">
<div>Adresse: {client.address}</div>
<div>Letzte Verbindung: {formatDate(client.latestHandshakeAt)}</div>
<div>
Traffic: ↑ {formatBytes(client.transferTx)} / ↓ {formatBytes(client.transferRx)}
</div>
</div>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleToggleClient(client.id, client.enabled)}
title={client.enabled ? 'Client deaktivieren' : 'Client aktivieren'}
>
{client.enabled ? (
<PowerOff className="h-4 w-4" />
) : (
<Power className="h-4 w-4" />
)}
</Button>
<Button variant="outline">
Server konfigurieren
<Button
variant="outline"
size="sm"
onClick={() => handleShowQR(client.id)}
title="QR-Code anzeigen"
>
<QrCode className="h-4 w-4" />
</Button>
<Button variant="outline">
Logs anzeigen
<Button
variant="outline"
size="sm"
onClick={() => handleDownloadConfig(client.id, client.name)}
title="Konfiguration herunterladen"
>
<FileDown className="h-4 w-4" />
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => handleDeleteClient(client.id, client.name)}
title="Client löschen"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</main>
</div>
)
}
// WireGuard Easy API Client
const WG_API_URL = process.env.WG_API_URL || 'http://localhost:51821'
const WG_PASSWORD = process.env.WG_PASSWORD || ''
interface WgClient {
id: string
name: string
enabled: boolean
address: string
publicKey: string
createdAt: string
updatedAt: string
latestHandshakeAt: string
transferRx: number
transferTx: number
persistentKeepalive: string
}
interface WgServer {
address: string
listenPort: number
publicKey: string
endpoint: string
dns: string
allowedIPs: string
createdAt: string
}
interface WgStats {
clients: WgClient[]
server: WgServer
}
interface CreateClientRequest {
name: string
allowedIPs?: string
}
interface CreateClientResponse {
id: string
name: string
config: string
qrCode: string
}
export class WgApiClient {
private baseUrl: string
private password: string
constructor(baseUrl: string = WG_API_URL, password: string = WG_PASSWORD) {
this.baseUrl = baseUrl.replace(/\/$/, '')
this.password = password
}
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const url = `${this.baseUrl}${endpoint}`
const headers = {
'Content-Type': 'application/json',
...options.headers,
}
// Add basic auth if password is set
if (this.password) {
headers['Authorization'] = `Basic ${Buffer.from(`:${this.password}`).toString('base64')}`
}
const response = await fetch(url, {
...options,
headers,
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`WG API Error: ${response.status} ${errorText}`)
}
return response.json()
}
async getStats(): Promise<WgStats> {
return this.request<WgStats>('/api/sessions')
}
async getClients(): Promise<WgClient[]> {
const stats = await this.getStats()
return stats.clients || []
}
async getServer(): Promise<WgServer> {
const stats = await this.getStats()
return stats.server
}
async createClient(data: CreateClientRequest): Promise<CreateClientResponse> {
return this.request<CreateClientResponse>('/api/users', {
method: 'POST',
body: JSON.stringify(data),
})
}
async deleteClient(id: string): Promise<void> {
await this.request(`/api/users/${id}`, {
method: 'DELETE',
})
}
async enableClient(id: string): Promise<void> {
await this.request(`/api/users/${id}/enable`, {
method: 'PUT',
})
}
async disableClient(id: string): Promise<void> {
await this.request(`/api/users/${id}/disable`, {
method: 'PUT',
})
}
async getClientConfig(id: string): Promise<string> {
const response = await fetch(`${this.baseUrl}/api/users/${id}/configuration`, {
headers: this.password ? {
'Authorization': `Basic ${Buffer.from(`:${this.password}`).toString('base64')}`
} : {},
})
if (!response.ok) {
throw new Error(`Failed to get config: ${response.status}`)
}
return response.text()
}
async getClientQR(id: string): Promise<string> {
const response = await fetch(`${this.baseUrl}/api/users/${id}/qr-code`, {
headers: this.password ? {
'Authorization': `Basic ${Buffer.from(`:${this.password}`).toString('base64')}`
} : {},
})
if (!response.ok) {
throw new Error(`Failed to get QR code: ${response.status}`)
}
return response.text()
}
}
export const wgApi = new WgApiClient()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment