From 6d8899a0789d0a962c7a4b53f0b0c860a8499839 Mon Sep 17 00:00:00 2001 From: datadunia Date: Sun, 3 May 2026 23:20:34 +0700 Subject: [PATCH] feat(app): add WGRplane web UI and backend features - Add Vue 3 frontend with glassmorphism design (Tailwind CSS) - Add Go backend handlers: auth, webhooks, stats, scheduler, validation - Add i18n support (EN, ID, ZH) - Add Swagger docs and API handlers - Add nftables integration and plugins support - Remove deprecated go.mod (migrated to wgrplane) --- app/Dockerfile | 15 + app/active.en.json | 4 + app/active.id.json | 4 + app/active.zh.json | 4 + app/auth.go | 146 ++ app/docs/docs.go | 1244 +++++++++++++++++ app/docs/swagger.json | 1220 ++++++++++++++++ app/docs/swagger.yaml | 777 ++++++++++ app/frontend/index.html | 2 +- app/frontend/package-lock.json | 697 ++++++++- app/frontend/package.json | 7 + app/frontend/postcss.config.js | 2 +- app/frontend/src/App.vue | 161 +++ .../src/components/ActionCheckboxes.vue | 61 + app/frontend/src/components/CIDRTagInput.vue | 93 ++ .../src/components/CustomBodyEditor.vue | 43 + .../src/components/HeaderKeyValue.vue | 76 + .../src/components/InternetToggle.vue | 29 + app/frontend/src/components/PeerTable.vue | 402 ++++++ .../src/components/TemplateDropdown.vue | 26 + app/frontend/src/components/WebhookForm.vue | 127 ++ app/frontend/src/components/WebhookList.vue | 92 ++ .../src/components/glass/GlassButton.vue | 8 +- .../src/components/glass/GlassCard.vue | 2 +- .../src/components/glass/GlassInput.vue | 8 +- .../src/components/glass/GlassToggle.vue | 10 +- app/frontend/src/composables/useTheme.ts | 36 + app/frontend/src/composables/useWebSocket.ts | 144 ++ app/frontend/src/i18n/index.ts | 15 + app/frontend/src/i18n/locales/en.json | 29 + app/frontend/src/i18n/locales/id.json | 29 + app/frontend/src/i18n/locales/zh.json | 29 + app/frontend/src/main.ts | 66 +- app/frontend/src/router/index.ts | 45 + app/frontend/src/shims-vue.d.ts | 5 + app/frontend/src/style.css | 295 +--- app/frontend/src/types/webhook.ts | 47 + app/frontend/src/views/HomeView.vue | 188 +++ app/frontend/src/views/PeersView.vue | 20 + app/frontend/src/views/ServerDetailView.vue | 28 + app/frontend/src/views/SettingsView.vue | 160 +++ app/frontend/src/views/WebhooksView.vue | 125 ++ app/frontend/tailwind.config.js | 1 + app/frontend/vite.config.ts | 6 + app/go.mod | 15 - app/go.sum | 47 +- app/handlers.go | 785 +++++++++++ app/i18n.go | 48 + app/plan.md | 56 +- app/plugins.go | 55 + app/scheduler.go | 95 ++ app/stats.go | 158 +++ app/validation.go | 42 + app/wgrplane.db | Bin 0 -> 40960 bytes 54 files changed, 7395 insertions(+), 434 deletions(-) create mode 100644 app/Dockerfile create mode 100644 app/active.en.json create mode 100644 app/active.id.json create mode 100644 app/active.zh.json create mode 100644 app/auth.go create mode 100644 app/docs/docs.go create mode 100644 app/docs/swagger.json create mode 100644 app/docs/swagger.yaml create mode 100644 app/frontend/src/App.vue create mode 100644 app/frontend/src/components/ActionCheckboxes.vue create mode 100644 app/frontend/src/components/CIDRTagInput.vue create mode 100644 app/frontend/src/components/CustomBodyEditor.vue create mode 100644 app/frontend/src/components/HeaderKeyValue.vue create mode 100644 app/frontend/src/components/InternetToggle.vue create mode 100644 app/frontend/src/components/PeerTable.vue create mode 100644 app/frontend/src/components/TemplateDropdown.vue create mode 100644 app/frontend/src/components/WebhookForm.vue create mode 100644 app/frontend/src/components/WebhookList.vue create mode 100644 app/frontend/src/composables/useTheme.ts create mode 100644 app/frontend/src/composables/useWebSocket.ts create mode 100644 app/frontend/src/i18n/index.ts create mode 100644 app/frontend/src/i18n/locales/en.json create mode 100644 app/frontend/src/i18n/locales/id.json create mode 100644 app/frontend/src/i18n/locales/zh.json create mode 100644 app/frontend/src/router/index.ts create mode 100644 app/frontend/src/shims-vue.d.ts create mode 100644 app/frontend/src/types/webhook.ts create mode 100644 app/frontend/src/views/HomeView.vue create mode 100644 app/frontend/src/views/PeersView.vue create mode 100644 app/frontend/src/views/ServerDetailView.vue create mode 100644 app/frontend/src/views/SettingsView.vue create mode 100644 app/frontend/src/views/WebhooksView.vue create mode 100644 app/frontend/vite.config.ts delete mode 100644 app/go.mod create mode 100644 app/handlers.go create mode 100644 app/i18n.go create mode 100644 app/plugins.go create mode 100644 app/scheduler.go create mode 100644 app/stats.go create mode 100644 app/validation.go create mode 100644 app/wgrplane.db diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..4ab7694 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,15 @@ +# Build stage +FROM golang:1.20-alpine AS builder +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN go build -o wgrplane ./... + +# Run stage +FROM alpine:3.18 +RUN apk --no-cache add ca-certificates +WORKDIR /app +COPY --from=builder /app/wgrplane . +EXPOSE 8080 +CMD ["./wgrplane"] diff --git a/app/active.en.json b/app/active.en.json new file mode 100644 index 0000000..fca757d --- /dev/null +++ b/app/active.en.json @@ -0,0 +1,4 @@ +{ + "greeting": "Hello", + "farewell": "Goodbye" +} diff --git a/app/active.id.json b/app/active.id.json new file mode 100644 index 0000000..d4e6494 --- /dev/null +++ b/app/active.id.json @@ -0,0 +1,4 @@ +{ + "greeting": "Halo", + "farewell": "Selamat tinggal" +} diff --git a/app/active.zh.json b/app/active.zh.json new file mode 100644 index 0000000..6bb33a1 --- /dev/null +++ b/app/active.zh.json @@ -0,0 +1,4 @@ +{ + "greeting": "你好", + "farewell": "再见" +} diff --git a/app/auth.go b/app/auth.go new file mode 100644 index 0000000..06e75a6 --- /dev/null +++ b/app/auth.go @@ -0,0 +1,146 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + "time" + + jwt "github.com/golang-jwt/jwt/v5" + "github.com/pquerna/otp/totp" +) + +// Simple in-memory storage for TOTPs per user. In production, use a DB. +var totpSecrets = map[string]string{} + +// API key (default) - can be overridden by WG_API_KEY env var. +var apiKey string + +// JWT secret (default) - can be overridden by JWT_SECRET env var. +var jwtSecret string + +func init() { + apiKey = os.Getenv("WG_API_KEY") + if apiKey == "" { + apiKey = "test-api-key" // default for testing + } + jwtSecret = os.Getenv("JWT_SECRET") + if jwtSecret == "" { + jwtSecret = "secret" // default for testing + } + // Preload a test user so JWTs can be generated in tests if needed + if _, ok := totpSecrets["test"]; !ok { + // generate a random-looking secret for test user if desired + // but we won't force it here; user can call /auth/setup-totp?user=test + } +} + +type Claims struct { + Username string `json:"username"` + jwt.RegisteredClaims +} + +// generateJWT creates a short-lived JWT for a given username +func generateJWT(username string) (string, error) { + claims := &Claims{ + Username: username, + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: "wgrplane", + ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(jwtSecret)) +} + +func parseJWT(tokenStr string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(jwtSecret), nil + }) + if err != nil { + return nil, err + } + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + return nil, fmt.Errorf("invalid token") +} + +// totpSetupHandler returns a new TOTP secret and provisioning URI for a user +func totpSetupHandler(w http.ResponseWriter, r *http.Request) { + user := r.URL.Query().Get("user") + if user == "" { + http.Error(w, "missing user", http.StatusBadRequest) + return + } + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: "WGRplane", + AccountName: user, + }) + if err != nil { + http.Error(w, "failed to generate secret", http.StatusInternalServerError) + return + } + secret := key.Secret() + totpSecrets[user] = secret + resp := map[string]string{ + "secret": secret, + "provisioning_uri": key.URL(), + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(resp) +} + +// AuthMiddleware protects selected routes with API Key or JWT + optional TOTP +func AuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 1) API Key path + if key := r.Header.Get("wg-rplane-datadunia"); key != "" { + if subtleConstantTimeEquals(key, apiKey) { + // If user has a TOTP, require current OTP in header + // The username isn't known from API Key alone; skip TOTP check here + next.ServeHTTP(w, r) + return + } + } + + // 2) JWT path + authHeader := r.Header.Get("Authorization") + if strings.HasPrefix(authHeader, "Bearer ") { + token := strings.TrimPrefix(authHeader, "Bearer ") + claims, err := parseJWT(token) + if err != nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + // If user has a TOTp secret, verify it + if secret, ok := totpSecrets[claims.Username]; ok { + otp := r.Header.Get("X-TOTP") + if otp == "" || !totp.Validate(otp, secret) { + http.Error(w, "Unauthorized (TOTp)", http.StatusUnauthorized) + return + } + } + // Attach username to context for downstream handlers if needed + next.ServeHTTP(w, r) + return + } + + // No valid auth provided + http.Error(w, "Unauthorized", http.StatusUnauthorized) + }) +} + +// helper for constant-time string comparison +func subtleConstantTimeEquals(a, b string) bool { + if len(a) != len(b) { + return false + } + var diff byte + for i := 0; i < len(a); i++ { + diff |= a[i] ^ b[i] + } + return diff == 0 +} diff --git a/app/docs/docs.go b/app/docs/docs.go new file mode 100644 index 0000000..57406ab --- /dev/null +++ b/app/docs/docs.go @@ -0,0 +1,1244 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": { + "name": "API Support" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/peers/{id}": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Updates a peer and computes diffs to apply incremental nftables changes.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "peers" + ], + "summary": "Update a peer", + "parameters": [ + { + "type": "string", + "description": "Peer ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated peer fields", + "name": "peer", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.Peer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Peer" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Deletes a peer, cleans up nftables rules, and triggers webhooks.", + "produces": [ + "application/json" + ], + "tags": [ + "peers" + ], + "summary": "Delete a peer", + "parameters": [ + { + "type": "string", + "description": "Peer ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No content" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/peers/{id}/config": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Downloads the WireGuard .conf file for a specific peer.", + "produces": [ + "text/plain" + ], + "tags": [ + "peers" + ], + "summary": "Get peer config", + "parameters": [ + { + "type": "string", + "description": "Peer ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "WireGuard configuration file", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/peers/{id}/qrcode": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Returns a QR code PNG image of the peer config for mobile import.", + "produces": [ + "image/png" + ], + "tags": [ + "peers" + ], + "summary": "Get peer QR code", + "parameters": [ + { + "type": "string", + "description": "Peer ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "QR code PNG image", + "schema": { + "type": "file" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/servers": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Returns a list of all registered WireGuard servers.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "servers" + ], + "summary": "List all servers", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Server" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Creates a new WireGuard server with the provided configuration.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "servers" + ], + "summary": "Create a server", + "parameters": [ + { + "description": "Server configuration", + "name": "server", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.Server" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/main.Server" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/servers/{id}": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Returns a single WireGuard server by its ID.", + "produces": [ + "application/json" + ], + "tags": [ + "servers" + ], + "summary": "Get a server", + "parameters": [ + { + "type": "string", + "description": "Server ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Server" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Updates fields of an existing WireGuard server.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "servers" + ], + "summary": "Update a server", + "parameters": [ + { + "type": "string", + "description": "Server ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated server fields", + "name": "server", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.Server" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Server" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Deletes a WireGuard server and cascade-removes its peers and webhooks.", + "produces": [ + "application/json" + ], + "tags": [ + "servers" + ], + "summary": "Delete a server", + "parameters": [ + { + "type": "string", + "description": "Server ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No content" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/servers/{id}/peers": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Returns all peers belonging to a specific WireGuard server.", + "produces": [ + "application/json" + ], + "tags": [ + "peers" + ], + "summary": "List server peers", + "parameters": [ + { + "type": "string", + "description": "Server ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Peer" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Creates a new WireGuard peer. Applies nftables rules in forward mode or triggers webhooks in standalone mode.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "peers" + ], + "summary": "Create a peer", + "parameters": [ + { + "type": "string", + "description": "Server ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Peer configuration", + "name": "peer", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.Peer" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/main.Peer" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/servers/{id}/stats": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Returns per-server statistics including peer count and webhook count.", + "produces": [ + "application/json" + ], + "tags": [ + "stats" + ], + "summary": "Get server stats", + "parameters": [ + { + "type": "string", + "description": "Server ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/servers/{id}/webhooks": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Returns all webhooks configured for a specific WireGuard server.", + "produces": [ + "application/json" + ], + "tags": [ + "webhooks" + ], + "summary": "List server webhooks", + "parameters": [ + { + "type": "string", + "description": "Server ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Webhook" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Creates a new webhook configuration for a WireGuard server.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "webhooks" + ], + "summary": "Create a webhook", + "parameters": [ + { + "type": "string", + "description": "Server ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Webhook configuration", + "name": "webhook", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.Webhook" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/main.Webhook" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/settings/smtp": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Returns the current SMTP configuration for email notifications.", + "produces": [ + "application/json" + ], + "tags": [ + "settings" + ], + "summary": "Get SMTP settings", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.SMTPSettings" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Saves or updates SMTP configuration for email notifications.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "settings" + ], + "summary": "Save SMTP settings", + "parameters": [ + { + "description": "SMTP configuration", + "name": "settings", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.SMTPSettings" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.SMTPSettings" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/stats": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Returns global statistics including total servers, peers, and webhooks.", + "produces": [ + "application/json" + ], + "tags": [ + "stats" + ], + "summary": "Get global stats", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + } + } + } + } + }, + "/api/webhooks/{id}": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Deletes a webhook by its ID.", + "produces": [ + "application/json" + ], + "tags": [ + "webhooks" + ], + "summary": "Delete a webhook", + "parameters": [ + { + "type": "string", + "description": "Webhook ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No content" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + }, + "definitions": { + "main.Peer": { + "type": "object", + "properties": { + "allowAccess": { + "type": "array", + "items": { + "type": "integer" + } + }, + "allowInternet": { + "type": "boolean" + }, + "createdAt": { + "type": "string" + }, + "currentDataUsageBytes": { + "description": "CurrentDataUsageBytes tracks the amount of data used by this peer (in bytes)", + "type": "integer", + "format": "int64" + }, + "dataLimitGB": { + "description": "DataLimitGB defines the monthly data limit per peer (in GB). 0 means unlimited.", + "type": "integer", + "format": "int64" + }, + "enabled": { + "description": "Enabled indicates whether the peer is active. Auto-restrict disables the peer if over the limit.", + "type": "boolean" + }, + "expiresAt": { + "description": "ExpiresAt defines when this peer should be considered expired and eligible for auto-deletion", + "type": "string" + }, + "id": { + "type": "integer" + }, + "ip": { + "type": "string" + }, + "publicKey": { + "type": "string" + }, + "serverID": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + } + } + }, + "main.SMTPSettings": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "fromEmail": { + "type": "string" + }, + "fromName": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "server": { + "type": "string" + }, + "useAuth": { + "type": "boolean" + }, + "useTLS": { + "type": "boolean" + }, + "username": { + "type": "string" + } + } + }, + "main.Server": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "endpoint": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "mode": { + "type": "string" + }, + "name": { + "type": "string" + }, + "peers": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Peer" + } + }, + "publicKey": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "webhooks": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Webhook" + } + } + } + }, + "main.Webhook": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "customBody": { + "type": "string" + }, + "customHeaders": { + "type": "array", + "items": { + "type": "integer" + } + }, + "defaultPayload": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "isEnabled": { + "type": "boolean" + }, + "isGlobal": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "serverID": { + "type": "integer" + }, + "subscribedActions": { + "type": "array", + "items": { + "type": "integer" + } + }, + "template": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "url": { + "type": "string" + }, + "verifySSL": { + "type": "boolean" + } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "wg-rplane-datadunia", + "in": "header" + }, + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "localhost:10087", + BasePath: "/", + Schemes: []string{}, + Title: "WGRplane API", + Description: "WireGuard Control Plane with Dynamic Policy Firewall. REST API for managing WireGuard servers, peers, webhooks, SMTP settings, and real-time stats.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/app/docs/swagger.json b/app/docs/swagger.json new file mode 100644 index 0000000..5fb2a5e --- /dev/null +++ b/app/docs/swagger.json @@ -0,0 +1,1220 @@ +{ + "swagger": "2.0", + "info": { + "description": "WireGuard Control Plane with Dynamic Policy Firewall. REST API for managing WireGuard servers, peers, webhooks, SMTP settings, and real-time stats.", + "title": "WGRplane API", + "contact": { + "name": "API Support" + }, + "version": "1.0" + }, + "host": "localhost:10087", + "basePath": "/", + "paths": { + "/api/peers/{id}": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Updates a peer and computes diffs to apply incremental nftables changes.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "peers" + ], + "summary": "Update a peer", + "parameters": [ + { + "type": "string", + "description": "Peer ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated peer fields", + "name": "peer", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.Peer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Peer" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Deletes a peer, cleans up nftables rules, and triggers webhooks.", + "produces": [ + "application/json" + ], + "tags": [ + "peers" + ], + "summary": "Delete a peer", + "parameters": [ + { + "type": "string", + "description": "Peer ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No content" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/peers/{id}/config": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Downloads the WireGuard .conf file for a specific peer.", + "produces": [ + "text/plain" + ], + "tags": [ + "peers" + ], + "summary": "Get peer config", + "parameters": [ + { + "type": "string", + "description": "Peer ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "WireGuard configuration file", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/peers/{id}/qrcode": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Returns a QR code PNG image of the peer config for mobile import.", + "produces": [ + "image/png" + ], + "tags": [ + "peers" + ], + "summary": "Get peer QR code", + "parameters": [ + { + "type": "string", + "description": "Peer ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "QR code PNG image", + "schema": { + "type": "file" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/servers": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Returns a list of all registered WireGuard servers.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "servers" + ], + "summary": "List all servers", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Server" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Creates a new WireGuard server with the provided configuration.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "servers" + ], + "summary": "Create a server", + "parameters": [ + { + "description": "Server configuration", + "name": "server", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.Server" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/main.Server" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/servers/{id}": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Returns a single WireGuard server by its ID.", + "produces": [ + "application/json" + ], + "tags": [ + "servers" + ], + "summary": "Get a server", + "parameters": [ + { + "type": "string", + "description": "Server ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Server" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Updates fields of an existing WireGuard server.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "servers" + ], + "summary": "Update a server", + "parameters": [ + { + "type": "string", + "description": "Server ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated server fields", + "name": "server", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.Server" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Server" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Deletes a WireGuard server and cascade-removes its peers and webhooks.", + "produces": [ + "application/json" + ], + "tags": [ + "servers" + ], + "summary": "Delete a server", + "parameters": [ + { + "type": "string", + "description": "Server ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No content" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/servers/{id}/peers": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Returns all peers belonging to a specific WireGuard server.", + "produces": [ + "application/json" + ], + "tags": [ + "peers" + ], + "summary": "List server peers", + "parameters": [ + { + "type": "string", + "description": "Server ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Peer" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Creates a new WireGuard peer. Applies nftables rules in forward mode or triggers webhooks in standalone mode.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "peers" + ], + "summary": "Create a peer", + "parameters": [ + { + "type": "string", + "description": "Server ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Peer configuration", + "name": "peer", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.Peer" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/main.Peer" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/servers/{id}/stats": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Returns per-server statistics including peer count and webhook count.", + "produces": [ + "application/json" + ], + "tags": [ + "stats" + ], + "summary": "Get server stats", + "parameters": [ + { + "type": "string", + "description": "Server ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/servers/{id}/webhooks": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Returns all webhooks configured for a specific WireGuard server.", + "produces": [ + "application/json" + ], + "tags": [ + "webhooks" + ], + "summary": "List server webhooks", + "parameters": [ + { + "type": "string", + "description": "Server ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Webhook" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Creates a new webhook configuration for a WireGuard server.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "webhooks" + ], + "summary": "Create a webhook", + "parameters": [ + { + "type": "string", + "description": "Server ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Webhook configuration", + "name": "webhook", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.Webhook" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/main.Webhook" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/settings/smtp": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Returns the current SMTP configuration for email notifications.", + "produces": [ + "application/json" + ], + "tags": [ + "settings" + ], + "summary": "Get SMTP settings", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.SMTPSettings" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Saves or updates SMTP configuration for email notifications.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "settings" + ], + "summary": "Save SMTP settings", + "parameters": [ + { + "description": "SMTP configuration", + "name": "settings", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.SMTPSettings" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.SMTPSettings" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/stats": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Returns global statistics including total servers, peers, and webhooks.", + "produces": [ + "application/json" + ], + "tags": [ + "stats" + ], + "summary": "Get global stats", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + } + } + } + } + }, + "/api/webhooks/{id}": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "Deletes a webhook by its ID.", + "produces": [ + "application/json" + ], + "tags": [ + "webhooks" + ], + "summary": "Delete a webhook", + "parameters": [ + { + "type": "string", + "description": "Webhook ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No content" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + }, + "definitions": { + "main.Peer": { + "type": "object", + "properties": { + "allowAccess": { + "type": "array", + "items": { + "type": "integer" + } + }, + "allowInternet": { + "type": "boolean" + }, + "createdAt": { + "type": "string" + }, + "currentDataUsageBytes": { + "description": "CurrentDataUsageBytes tracks the amount of data used by this peer (in bytes)", + "type": "integer", + "format": "int64" + }, + "dataLimitGB": { + "description": "DataLimitGB defines the monthly data limit per peer (in GB). 0 means unlimited.", + "type": "integer", + "format": "int64" + }, + "enabled": { + "description": "Enabled indicates whether the peer is active. Auto-restrict disables the peer if over the limit.", + "type": "boolean" + }, + "expiresAt": { + "description": "ExpiresAt defines when this peer should be considered expired and eligible for auto-deletion", + "type": "string" + }, + "id": { + "type": "integer" + }, + "ip": { + "type": "string" + }, + "publicKey": { + "type": "string" + }, + "serverID": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + } + } + }, + "main.SMTPSettings": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "fromEmail": { + "type": "string" + }, + "fromName": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "server": { + "type": "string" + }, + "useAuth": { + "type": "boolean" + }, + "useTLS": { + "type": "boolean" + }, + "username": { + "type": "string" + } + } + }, + "main.Server": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "endpoint": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "mode": { + "type": "string" + }, + "name": { + "type": "string" + }, + "peers": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Peer" + } + }, + "publicKey": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "webhooks": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Webhook" + } + } + } + }, + "main.Webhook": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "customBody": { + "type": "string" + }, + "customHeaders": { + "type": "array", + "items": { + "type": "integer" + } + }, + "defaultPayload": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "isEnabled": { + "type": "boolean" + }, + "isGlobal": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "serverID": { + "type": "integer" + }, + "subscribedActions": { + "type": "array", + "items": { + "type": "integer" + } + }, + "template": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "url": { + "type": "string" + }, + "verifySSL": { + "type": "boolean" + } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "wg-rplane-datadunia", + "in": "header" + }, + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/app/docs/swagger.yaml b/app/docs/swagger.yaml new file mode 100644 index 0000000..a3264f5 --- /dev/null +++ b/app/docs/swagger.yaml @@ -0,0 +1,777 @@ +basePath: / +definitions: + main.Peer: + properties: + allowAccess: + items: + type: integer + type: array + allowInternet: + type: boolean + createdAt: + type: string + currentDataUsageBytes: + description: CurrentDataUsageBytes tracks the amount of data used by this + peer (in bytes) + format: int64 + type: integer + dataLimitGB: + description: DataLimitGB defines the monthly data limit per peer (in GB). + 0 means unlimited. + format: int64 + type: integer + enabled: + description: Enabled indicates whether the peer is active. Auto-restrict disables + the peer if over the limit. + type: boolean + expiresAt: + description: ExpiresAt defines when this peer should be considered expired + and eligible for auto-deletion + type: string + id: + type: integer + ip: + type: string + publicKey: + type: string + serverID: + type: integer + updatedAt: + type: string + type: object + main.SMTPSettings: + properties: + enabled: + type: boolean + fromEmail: + type: string + fromName: + type: string + id: + type: integer + password: + type: string + port: + type: integer + server: + type: string + useAuth: + type: boolean + useTLS: + type: boolean + username: + type: string + type: object + main.Server: + properties: + createdAt: + type: string + endpoint: + type: string + id: + type: integer + mode: + type: string + name: + type: string + peers: + items: + $ref: '#/definitions/main.Peer' + type: array + publicKey: + type: string + updatedAt: + type: string + webhooks: + items: + $ref: '#/definitions/main.Webhook' + type: array + type: object + main.Webhook: + properties: + createdAt: + type: string + customBody: + type: string + customHeaders: + items: + type: integer + type: array + defaultPayload: + type: string + id: + type: integer + isEnabled: + type: boolean + isGlobal: + type: boolean + name: + type: string + serverID: + type: integer + subscribedActions: + items: + type: integer + type: array + template: + type: string + updatedAt: + type: string + url: + type: string + verifySSL: + type: boolean + type: object +host: localhost:10087 +info: + contact: + name: API Support + description: WireGuard Control Plane with Dynamic Policy Firewall. REST API for + managing WireGuard servers, peers, webhooks, SMTP settings, and real-time stats. + title: WGRplane API + version: "1.0" +paths: + /api/peers/{id}: + delete: + description: Deletes a peer, cleans up nftables rules, and triggers webhooks. + parameters: + - description: Peer ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No content + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: Delete a peer + tags: + - peers + put: + consumes: + - application/json + description: Updates a peer and computes diffs to apply incremental nftables + changes. + parameters: + - description: Peer ID + in: path + name: id + required: true + type: string + - description: Updated peer fields + in: body + name: peer + required: true + schema: + $ref: '#/definitions/main.Peer' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.Peer' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: Update a peer + tags: + - peers + /api/peers/{id}/config: + get: + description: Downloads the WireGuard .conf file for a specific peer. + parameters: + - description: Peer ID + in: path + name: id + required: true + type: string + produces: + - text/plain + responses: + "200": + description: WireGuard configuration file + schema: + type: string + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: Get peer config + tags: + - peers + /api/peers/{id}/qrcode: + get: + description: Returns a QR code PNG image of the peer config for mobile import. + parameters: + - description: Peer ID + in: path + name: id + required: true + type: string + produces: + - image/png + responses: + "200": + description: QR code PNG image + schema: + type: file + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: Get peer QR code + tags: + - peers + /api/servers: + get: + consumes: + - application/json + description: Returns a list of all registered WireGuard servers. + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/main.Server' + type: array + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: List all servers + tags: + - servers + post: + consumes: + - application/json + description: Creates a new WireGuard server with the provided configuration. + parameters: + - description: Server configuration + in: body + name: server + required: true + schema: + $ref: '#/definitions/main.Server' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/main.Server' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: Create a server + tags: + - servers + /api/servers/{id}: + delete: + description: Deletes a WireGuard server and cascade-removes its peers and webhooks. + parameters: + - description: Server ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No content + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: Delete a server + tags: + - servers + get: + description: Returns a single WireGuard server by its ID. + parameters: + - description: Server ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.Server' + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: Get a server + tags: + - servers + put: + consumes: + - application/json + description: Updates fields of an existing WireGuard server. + parameters: + - description: Server ID + in: path + name: id + required: true + type: string + - description: Updated server fields + in: body + name: server + required: true + schema: + $ref: '#/definitions/main.Server' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.Server' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: Update a server + tags: + - servers + /api/servers/{id}/peers: + get: + description: Returns all peers belonging to a specific WireGuard server. + parameters: + - description: Server ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/main.Peer' + type: array + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: List server peers + tags: + - peers + post: + consumes: + - application/json + description: Creates a new WireGuard peer. Applies nftables rules in forward + mode or triggers webhooks in standalone mode. + parameters: + - description: Server ID + in: path + name: id + required: true + type: string + - description: Peer configuration + in: body + name: peer + required: true + schema: + $ref: '#/definitions/main.Peer' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/main.Peer' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: Create a peer + tags: + - peers + /api/servers/{id}/stats: + get: + description: Returns per-server statistics including peer count and webhook + count. + parameters: + - description: Server ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: Get server stats + tags: + - stats + /api/servers/{id}/webhooks: + get: + description: Returns all webhooks configured for a specific WireGuard server. + parameters: + - description: Server ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/main.Webhook' + type: array + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: List server webhooks + tags: + - webhooks + post: + consumes: + - application/json + description: Creates a new webhook configuration for a WireGuard server. + parameters: + - description: Server ID + in: path + name: id + required: true + type: string + - description: Webhook configuration + in: body + name: webhook + required: true + schema: + $ref: '#/definitions/main.Webhook' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/main.Webhook' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: Create a webhook + tags: + - webhooks + /api/settings/smtp: + get: + description: Returns the current SMTP configuration for email notifications. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.SMTPSettings' + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: Get SMTP settings + tags: + - settings + post: + consumes: + - application/json + description: Saves or updates SMTP configuration for email notifications. + parameters: + - description: SMTP configuration + in: body + name: settings + required: true + schema: + $ref: '#/definitions/main.SMTPSettings' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.SMTPSettings' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: Save SMTP settings + tags: + - settings + /api/stats: + get: + description: Returns global statistics including total servers, peers, and webhooks. + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + format: int64 + type: integer + type: object + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: Get global stats + tags: + - stats + /api/webhooks/{id}: + delete: + description: Deletes a webhook by its ID. + parameters: + - description: Webhook ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No content + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: Delete a webhook + tags: + - webhooks +securityDefinitions: + ApiKeyAuth: + in: header + name: wg-rplane-datadunia + type: apiKey + BearerAuth: + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/app/frontend/index.html b/app/frontend/index.html index 096d706..bd07f65 100644 --- a/app/frontend/index.html +++ b/app/frontend/index.html @@ -4,7 +4,7 @@ - frontend + WGRplane - WireGuard Control
diff --git a/app/frontend/package-lock.json b/app/frontend/package-lock.json index 1abe646..f343c36 100644 --- a/app/frontend/package-lock.json +++ b/app/frontend/package-lock.json @@ -11,19 +11,38 @@ "@headlessui/vue": "^1.7.23", "@vueuse/core": "^14.3.0", "chart.js": "^4.5.1", + "vue": "^3.5.33", "vue-chartjs": "^5.3.3", + "vue-i18n": "^9.14.5", "vue-router": "^4.6.4", "vue-sonner": "^2.0.9" }, "devDependencies": { + "@tailwindcss/postcss": "^4.2.4", + "@vitejs/plugin-vue": "^6.0.6", + "autoprefixer": "^10.5.0", + "postcss": "^8.5.13", + "tailwindcss": "^4.2.4", "typescript": "~6.0.2", "vite": "^8.0.10" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -31,7 +50,6 @@ "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -39,7 +57,6 @@ "node_modules/@babel/parser": { "version": "7.29.3", "license": "MIT", - "peer": true, "dependencies": { "@babel/types": "^7.29.0" }, @@ -53,7 +70,6 @@ "node_modules/@babel/types": { "version": "7.29.0", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" @@ -111,10 +127,96 @@ "vue": "^3.2.0" } }, + "node_modules/@intlify/core-base": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.5.tgz", + "integrity": "sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==", + "license": "MIT", + "dependencies": { + "@intlify/message-compiler": "9.14.5", + "@intlify/shared": "9.14.5" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.5.tgz", + "integrity": "sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==", + "license": "MIT", + "dependencies": { + "@intlify/shared": "9.14.5", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.5.tgz", + "integrity": "sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", - "peer": true + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } }, "node_modules/@kurkle/color": { "version": "0.3.4", @@ -427,6 +529,289 @@ "dev": true, "license": "MIT" }, + "node_modules/@tailwindcss/node": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", + "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.4" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", + "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-x64": "4.2.4", + "@tailwindcss/oxide-freebsd-x64": "4.2.4", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-x64-musl": "4.2.4", + "@tailwindcss/oxide-wasm32-wasi": "4.2.4", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", + "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", + "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", + "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", + "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", + "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", + "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", + "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", + "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", + "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", + "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", + "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", + "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.4.tgz", + "integrity": "sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.4", + "@tailwindcss/oxide": "4.2.4", + "postcss": "^8.5.6", + "tailwindcss": "4.2.4" + } + }, "node_modules/@tanstack/virtual-core": { "version": "3.14.0", "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.14.0.tgz", @@ -470,10 +855,33 @@ "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", "license": "MIT" }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz", + "integrity": "sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.13" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitejs/plugin-vue/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz", + "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==", + "dev": true, + "license": "MIT" + }, "node_modules/@vue/compiler-core": { "version": "3.5.33", "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.29.2", "@vue/shared": "3.5.33", @@ -485,7 +893,6 @@ "node_modules/@vue/compiler-dom": { "version": "3.5.33", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-core": "3.5.33", "@vue/shared": "3.5.33" @@ -494,7 +901,6 @@ "node_modules/@vue/compiler-sfc": { "version": "3.5.33", "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.29.2", "@vue/compiler-core": "3.5.33", @@ -510,7 +916,6 @@ "node_modules/@vue/compiler-ssr": { "version": "3.5.33", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.33", "@vue/shared": "3.5.33" @@ -525,7 +930,6 @@ "node_modules/@vue/reactivity": { "version": "3.5.33", "license": "MIT", - "peer": true, "dependencies": { "@vue/shared": "3.5.33" } @@ -533,7 +937,6 @@ "node_modules/@vue/runtime-core": { "version": "3.5.33", "license": "MIT", - "peer": true, "dependencies": { "@vue/reactivity": "3.5.33", "@vue/shared": "3.5.33" @@ -542,7 +945,6 @@ "node_modules/@vue/runtime-dom": { "version": "3.5.33", "license": "MIT", - "peer": true, "dependencies": { "@vue/reactivity": "3.5.33", "@vue/runtime-core": "3.5.33", @@ -553,7 +955,6 @@ "node_modules/@vue/server-renderer": { "version": "3.5.33", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-ssr": "3.5.33", "@vue/shared": "3.5.33" @@ -564,8 +965,7 @@ }, "node_modules/@vue/shared": { "version": "3.5.33", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@vueuse/core": { "version": "14.3.0", @@ -605,6 +1005,111 @@ "vue": "^3.5.0" } }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.25", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.25.tgz", + "integrity": "sha512-QO/VHsXCQdnzADMfmkeOPvHdIAkoB7i0/rGjINPJEetLx75hNttVWGQ/jycHUDP9zZ9rupbm60WRxcwViB0MiA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chart.js": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", @@ -619,8 +1124,7 @@ }, "node_modules/csstype": { "version": "3.2.3", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/detect-libc": { "version": "2.1.2", @@ -630,10 +1134,30 @@ "node": ">=8" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.349", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz", + "integrity": "sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", + "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "7.0.1", "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=0.12" }, @@ -641,10 +1165,19 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/estree-walker": { "version": "2.0.2", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fdir": { "version": "6.5.0", @@ -662,6 +1195,20 @@ } } }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -677,6 +1224,23 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/lightningcss": { "version": "1.32.0", "dev": true, @@ -949,7 +1513,6 @@ "node_modules/magic-string": { "version": "0.30.21", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } @@ -970,6 +1533,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "license": "ISC" @@ -987,6 +1557,8 @@ }, "node_modules/postcss": { "version": "8.5.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", + "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", "funding": [ { "type": "opencollective", @@ -1011,6 +1583,13 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/rolldown": { "version": "1.0.0-rc.17", "dev": true, @@ -1050,6 +1629,27 @@ "node": ">=0.10.0" } }, + "node_modules/tailwindcss": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", + "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "dev": true, @@ -1085,6 +1685,37 @@ "node": ">=14.17" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/vite": { "version": "8.0.10", "dev": true, @@ -1163,8 +1794,9 @@ }, "node_modules/vue": { "version": "3.5.33", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.33.tgz", + "integrity": "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.33", "@vue/compiler-sfc": "3.5.33", @@ -1191,6 +1823,27 @@ "vue": "^3.0.0-0 || ^2.7.0" } }, + "node_modules/vue-i18n": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.5.tgz", + "integrity": "sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==", + "deprecated": "v9 and v10 no longer supported. please migrate to v11. about maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "9.14.5", + "@intlify/shared": "9.14.5", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/vue-router": { "version": "4.6.4", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", diff --git a/app/frontend/package.json b/app/frontend/package.json index f4a6bb7..189153a 100644 --- a/app/frontend/package.json +++ b/app/frontend/package.json @@ -9,6 +9,11 @@ "preview": "vite preview" }, "devDependencies": { + "@tailwindcss/postcss": "^4.2.4", + "@vitejs/plugin-vue": "^6.0.6", + "autoprefixer": "^10.5.0", + "postcss": "^8.5.13", + "tailwindcss": "^4.2.4", "typescript": "~6.0.2", "vite": "^8.0.10" }, @@ -16,7 +21,9 @@ "@headlessui/vue": "^1.7.23", "@vueuse/core": "^14.3.0", "chart.js": "^4.5.1", + "vue": "^3.5.33", "vue-chartjs": "^5.3.3", + "vue-i18n": "^9.14.5", "vue-router": "^4.6.4", "vue-sonner": "^2.0.9" } diff --git a/app/frontend/postcss.config.js b/app/frontend/postcss.config.js index 2e7af2b..1c87846 100644 --- a/app/frontend/postcss.config.js +++ b/app/frontend/postcss.config.js @@ -1,6 +1,6 @@ export default { plugins: { - tailwindcss: {}, + '@tailwindcss/postcss': {}, autoprefixer: {}, }, } diff --git a/app/frontend/src/App.vue b/app/frontend/src/App.vue new file mode 100644 index 0000000..bfcad3b --- /dev/null +++ b/app/frontend/src/App.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/app/frontend/src/components/ActionCheckboxes.vue b/app/frontend/src/components/ActionCheckboxes.vue new file mode 100644 index 0000000..d6c00c1 --- /dev/null +++ b/app/frontend/src/components/ActionCheckboxes.vue @@ -0,0 +1,61 @@ + + + diff --git a/app/frontend/src/components/CIDRTagInput.vue b/app/frontend/src/components/CIDRTagInput.vue new file mode 100644 index 0000000..78747bd --- /dev/null +++ b/app/frontend/src/components/CIDRTagInput.vue @@ -0,0 +1,93 @@ + + + diff --git a/app/frontend/src/components/CustomBodyEditor.vue b/app/frontend/src/components/CustomBodyEditor.vue new file mode 100644 index 0000000..2cc19bb --- /dev/null +++ b/app/frontend/src/components/CustomBodyEditor.vue @@ -0,0 +1,43 @@ + + + diff --git a/app/frontend/src/components/HeaderKeyValue.vue b/app/frontend/src/components/HeaderKeyValue.vue new file mode 100644 index 0000000..e4f9cd6 --- /dev/null +++ b/app/frontend/src/components/HeaderKeyValue.vue @@ -0,0 +1,76 @@ + + + diff --git a/app/frontend/src/components/InternetToggle.vue b/app/frontend/src/components/InternetToggle.vue new file mode 100644 index 0000000..a4c68f5 --- /dev/null +++ b/app/frontend/src/components/InternetToggle.vue @@ -0,0 +1,29 @@ + + + diff --git a/app/frontend/src/components/PeerTable.vue b/app/frontend/src/components/PeerTable.vue new file mode 100644 index 0000000..8f09a4f --- /dev/null +++ b/app/frontend/src/components/PeerTable.vue @@ -0,0 +1,402 @@ + + + diff --git a/app/frontend/src/components/TemplateDropdown.vue b/app/frontend/src/components/TemplateDropdown.vue new file mode 100644 index 0000000..1975738 --- /dev/null +++ b/app/frontend/src/components/TemplateDropdown.vue @@ -0,0 +1,26 @@ + + + diff --git a/app/frontend/src/components/WebhookForm.vue b/app/frontend/src/components/WebhookForm.vue new file mode 100644 index 0000000..2ee02c4 --- /dev/null +++ b/app/frontend/src/components/WebhookForm.vue @@ -0,0 +1,127 @@ + + + diff --git a/app/frontend/src/components/WebhookList.vue b/app/frontend/src/components/WebhookList.vue new file mode 100644 index 0000000..8ff89e5 --- /dev/null +++ b/app/frontend/src/components/WebhookList.vue @@ -0,0 +1,92 @@ + + + diff --git a/app/frontend/src/components/glass/GlassButton.vue b/app/frontend/src/components/glass/GlassButton.vue index 3ff12ef..6d4fb55 100644 --- a/app/frontend/src/components/glass/GlassButton.vue +++ b/app/frontend/src/components/glass/GlassButton.vue @@ -1,8 +1,8 @@ diff --git a/app/frontend/src/components/glass/GlassCard.vue b/app/frontend/src/components/glass/GlassCard.vue index b3cab84..ac08aa6 100644 --- a/app/frontend/src/components/glass/GlassCard.vue +++ b/app/frontend/src/components/glass/GlassCard.vue @@ -1,5 +1,5 @@ diff --git a/app/frontend/src/components/glass/GlassInput.vue b/app/frontend/src/components/glass/GlassInput.vue index 7fe3803..08aa6b7 100644 --- a/app/frontend/src/components/glass/GlassInput.vue +++ b/app/frontend/src/components/glass/GlassInput.vue @@ -1,6 +1,6 @@ diff --git a/app/frontend/src/components/glass/GlassToggle.vue b/app/frontend/src/components/glass/GlassToggle.vue index de09663..e11688f 100644 --- a/app/frontend/src/components/glass/GlassToggle.vue +++ b/app/frontend/src/components/glass/GlassToggle.vue @@ -1,9 +1,9 @@