commit daffbc67dc1eb90c550dfeb0ec8b4658ee578b33 Author: Meninjar Mulyono Date: Wed Sep 24 18:42:16 2025 +0700 first commit diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..f127ea0 --- /dev/null +++ b/.air.toml @@ -0,0 +1,46 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = ".\\main.exe" + cmd = "make build" + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5cae497 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with "go test -c" +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +tmp/ + +# IDE specific files +.vscode +.idea + +# .env file +.env + +# Project build +main +*templ.go + +# OS X generated file +.DS_Store + diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..e001bf5 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,42 @@ +version: 2 +before: + hooks: + - go mod tidy + +env: + - PACKAGE_PATH=github.com///cmd + +builds: +- binary: "{{ .ProjectName }}" + main: ./cmd/api + goos: + - darwin + - linux + - windows + goarch: + - amd64 + - arm64 + env: + - CGO_ENABLED=0 + ldflags: + - -s -w -X {{.Env.PACKAGE_PATH}}={{.Version}} +release: + prerelease: auto + +universal_binaries: +- replace: true + +archives: + - name_template: > + {{- .ProjectName }}_{{- .Version }}_{{- title .Os }}_{{- if eq .Arch "amd64" }}x86_64{{- else if eq .Arch "386" }}i386{{- else }}{{ .Arch }}{{ end }}{{- if .Arm }}v{{ .Arm }}{{ end -}} + format_overrides: + - goos: windows + format: zip + builds_info: + group: root + owner: root + files: + - README.md + +checksum: + name_template: 'checksums.txt' diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4b4c9ce --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:1.24.4-alpine AS build + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN go build -o main cmd/api/main.go + +FROM alpine:3.20.1 AS prod +WORKDIR /app +COPY --from=build /app/main /app/main +COPY --from=build /app/.env /app/.env +EXPOSE 8080 +CMD ["./main"] + + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0c2392a --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +# Simple Makefile for a Go project + +# Build the application +all: build test + +build: + @echo "Building..." + + + @go build -o main.exe cmd/api/main.go + +# Run the application +run: + @go run cmd/api/main.go +# Create DB container +docker-run: + @docker compose up --build + +# Shutdown DB container +docker-down: + @docker compose down + +# Test the application +test: + @echo "Testing..." + @go test ./... -v +# Integrations Tests for the application +itest: + @echo "Running integration tests..." + @go test ./internal/database -v + +# Clean the binary +clean: + @echo "Cleaning..." + @rm -f main + +# Live Reload +watch: + @powershell -ExecutionPolicy Bypass -Command "if (Get-Command air -ErrorAction SilentlyContinue) { \ + air; \ + Write-Output 'Watching...'; \ + } else { \ + Write-Output 'Installing air...'; \ + go install github.com/air-verse/air@latest; \ + air; \ + Write-Output 'Watching...'; \ + }" + +.PHONY: all build run test clean watch docker-run docker-down itest diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a69478 --- /dev/null +++ b/README.md @@ -0,0 +1,689 @@ + +# ๐Ÿš€ WebSocket API Service - Real-Time Communication + +> **Modern WebSocket API service with advanced real-time communication, client management, and broadcasting capabilities** + +## ๐Ÿ“‘ Daftar Isi + +- [โœจ Fitur Utama](#-fitur-utama) +- [๐Ÿ—๏ธ Arsitektur](#%EF%B8%8F-arsitektur) +- [โšก Quick Start](#-quick-start) +- [๐Ÿ” Autentikasi](#-autentikasi) +- [๐Ÿ“Š API Endpoints](#-api-endpoints) +- [๐Ÿ› ๏ธ Development](#%EF%B8%8F-development) +- [๐Ÿš€ Deployment](#-deployment) +- [๐Ÿ“š Dokumentasi](#-dokumentasi) + +*** + +## โœจ Fitur Utama + +### Real-Time Communication + +- **๐Ÿ”„ WebSocket Server** - High-performance WebSocket server dengan auto-reconnect +- **๐Ÿ  Room Management** - Multi-room support untuk isolated communication +- **๐Ÿ‘ฅ Client Tracking** - Advanced client identification (IP-based, static ID, generated) +- **๐Ÿ“ก Real-time Broadcasting** - Server-initiated broadcasts ke semua atau specific clients +- **๐Ÿ’ฌ Direct Messaging** - Peer-to-peer messaging antar clients +- **๐Ÿ”„ Database Notifications** - PostgreSQL LISTEN/NOTIFY integration + +### Advanced Features + +- **๐Ÿ“Š Connection Monitoring** - Real-time statistics dan health monitoring +- **๐Ÿงน Auto Cleanup** - Automatic cleanup untuk inactive connections +- **๐Ÿ”’ Secure Authentication** - JWT-based authentication untuk WebSocket connections +- **๐Ÿ“ˆ Performance Metrics** - Built-in performance tracking dan analytics +- **๐ŸŽฏ Message Queue** - High-throughput message processing dengan worker pools +- **๐Ÿ” Activity Logging** - Comprehensive activity logging untuk debugging + +### Developer Experience + +- **๐Ÿ”ฅ Hot Reload** - Development dengan auto-restart +- **๐Ÿณ Docker Ready** - Easy deployment dengan Docker +- **โšก Code Generator** - Buat handler dan model otomatis +- **๐Ÿงช Testing Suite** - Unit dan integration tests +- **๐Ÿ“Š Health Monitoring** - Monitoring kesehatan aplikasi + +*** + +## ๐Ÿ—๏ธ Arsitektur WebSocket + +### WebSocket Architecture Layers + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ WebSocket Layer โ”‚ โ† websocket.go, broadcast.go +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Application Layer โ”‚ โ† handlers/, middleware/ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Domain Layer โ”‚ โ† models/, services/ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Infrastructure Layer โ”‚ โ† database/, external APIs +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Core Components + +``` +api-service/ +โ”œโ”€โ”€ ๐Ÿ“ cmd/api/ # ๐Ÿšช WebSocket server entry point +โ”œโ”€โ”€ ๐Ÿ“ internal/ # ๐Ÿ  Core WebSocket logic +โ”‚ โ”œโ”€โ”€ handlers/websocket/ # ๐Ÿ”„ WebSocket handlers & hub +โ”‚ โ”œโ”€โ”€ middleware/ # ๐Ÿ›ก๏ธ Auth & validation middleware +โ”‚ โ”œโ”€โ”€ models/ # ๐Ÿ“Š Data structures & validation +โ”‚ โ”œโ”€โ”€ routes/ # ๐Ÿ›ฃ๏ธ API routing +โ”‚ โ”œโ”€โ”€ services/ # ๐Ÿ’ผ Business logic services +โ”‚ โ””โ”€โ”€ database/ # ๐Ÿ’พ Database connections +โ”œโ”€โ”€ ๐Ÿ“ examples/clientsocket/ # ๐ŸŒ Vue.js WebSocket client +โ”œโ”€โ”€ ๐Ÿ“ docs/ # ๐Ÿ“š Swagger documentation +โ””โ”€โ”€ ๐Ÿ“ configs/ # โš™๏ธ Configuration files +``` + +### WebSocket Hub Architecture + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Hub โ”‚ โ† Central WebSocket manager +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ€ข Client Management โ”‚ โ† Register/unregister clients +โ”‚ โ€ข Message Broadcasting โ”‚ โ† Broadcast to clients/rooms +โ”‚ โ€ข Room Management โ”‚ โ† Multi-room support +โ”‚ โ€ข Connection Monitoring โ”‚ โ† Track active connections +โ”‚ โ€ข Database Notifications โ”‚ โ† PostgreSQL LISTEN/NOTIFY +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + + +*** + +## โšก Quick Start + +### 1๏ธโƒฃ Setup Environment (2 menit) + +```bash +# Clone repository +git clone +cd api-service + +# Setup environment +cp .env.example .env +``` + +### 2๏ธโƒฃ Pilih Method Setup + +**๐Ÿณ Docker (Recommended)** + +```bash +make docker-run +``` + +**๐Ÿ”ง Manual Setup** + +```bash +# Install dependencies +go mod download + +# Start server +go run cmd/api/main.go +``` + +### 3๏ธโƒฃ Setup WebSocket Client + +**Start Vue.js Client Example:** + +```bash +cd examples/clientsocket + +# Install dependencies +npm install + +# Start development server +npm run dev +``` + +### 4๏ธโƒฃ Verify Installation + +| Service | URL | Status | +| :-- | :-- | :-- | +| **WebSocket API** | ws://localhost:8080/api/v1/ws | โœ… | +| **WebSocket Client** | http://localhost:3000 | ๐ŸŒ | +| **API Documentation** | http://localhost:8080/swagger/index.html | ๐Ÿ“– | +| **Health Check** | http://localhost:8080/api/sistem/health | ๐Ÿ’š | + + +*** + +## ๐Ÿ” Autentikasi + +### Login \& Mendapatkan Token + +```bash +curl -X POST http://localhost:8080/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "username": "admin", + "password": "password" + }' +``` + +**Response:** + +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiIs...", + "expires_in": 3600, + "user": { + "id": "123", + "username": "admin", + "role": "admin" + } +} +``` + + +### Menggunakan Token + +```bash +curl -X GET http://localhost:8080/api/v1/products \ + -H "Authorization: Bearer " +``` + + +### Demo Accounts + +| Username | Password | Role | Akses | +| :-- | :-- | :-- | :-- | +| `admin` | `password` | Admin | Semua endpoint | +| `user` | `password` | User | Read-only | + + +*** + +## ๐Ÿ“Š API Endpoints + +### ๐Ÿ”„ WebSocket Endpoints + +#### Main WebSocket Connection + +| Protocol | Endpoint | Deskripsi | +| :-- | :-- | :-- | +| `WS` | `ws://localhost:8080/api/v1/ws` | WebSocket connection utama | + +**Connection Parameters:** + +```javascript +// Basic connection +const ws = new WebSocket('ws://localhost:8080/api/v1/ws'); +``` + +#### WebSocket Management API + +| Method | Endpoint | Deskripsi | +| :-- | :-- | :-- | +| `GET` | `/api/websocket/stats` | WebSocket connection statistics | +| `GET` | `/api/websocket/clients` | List all connected clients | +| `GET` | `/api/websocket/rooms` | List all active rooms | +| `POST` | `/api/websocket/broadcast` | Server-initiated broadcast | +| `POST` | `/api/websocket/broadcast/room/:room` | Broadcast to specific room | +| `POST` | `/api/websocket/send/:clientId` | Send message to specific client | +| `POST` | `/api/websocket/cleanup/inactive` | Cleanup inactive clients | + +### ๐ŸŒ Public REST Endpoints + +| Method | Endpoint | Deskripsi | +| :-- | :-- | :-- | +| `POST` | `/api/v1/auth/login` | Login pengguna | +| `POST` | `/api/v1/auth/register` | Registrasi pengguna baru | +| `GET` | `/api/sistem/health` | Status kesehatan API | +| `GET` | `/api/sistem/info` | System information | + +### ๐Ÿ”’ Protected REST Endpoints + +#### User Management + +| Method | Endpoint | Deskripsi | +| :-- | :-- | :-- | +| `GET` | `/api/v1/auth/me` | Profile pengguna | +| `PUT` | `/api/v1/auth/me` | Update profile | + +#### Retribusi Management + +| Method | Endpoint | Deskripsi | +| :-- | :-- | :-- | +| `GET` | `/api/v1/retribusi` | List semua retribusi | +| `GET` | `/api/v1/retribusi/dynamic` | Query dengan filter dinamis | +| `GET` | `/api/v1/retribusi/search` | Search retribusi advanced | +| `GET` | `/api/v1/retribusi/id/:id` | Detail retribusi by ID | +| `POST` | `/api/v1/retribusi` | Buat retribusi baru | +| `PUT` | `/api/v1/retribusi/id/:id` | Update retribusi | +| `DELETE` | `/api/v1/retribusi/id/:id` | Hapus retribusi | + + +*** + +## ๐Ÿ› ๏ธ Development + +### Code Generation (30 detik) + +**๐ŸŽฏ Generate CRUD Lengkap** + +```bash +# Generate handler untuk entity baru +go run tools/general/generate-handler.go product get post put delete + +# Generate dengan fitur advanced +go run tools/general/generate-handler.go orders get post put delete dynamic search stats +``` + +**๐Ÿฅ Generate BPJS Handler** + +```bash +# Single service +go run tools/bpjs/generate-bpjs-handler.go tools/bpjs/reference/peserta get + +# Semua service dari config +go run tools/bpjs/generate-handler.go tools/bpjs/services-config-bpjs.yaml +``` + +**๐Ÿฉบ Generate SATUSEHAT Handler** + +```bash +go run tools/satusehat/generate-satusehat-handler.go tools/satusehat/services-config-satusehat.yaml patient +``` + + +### Development Commands + +```bash +# ๐Ÿ”ฅ Development dengan hot reload +make watch + +# ๐Ÿงช Testing +make test # Unit tests +make itest # Integration tests +make test-all # Semua tests + +# ๐Ÿ“– Update dokumentasi +make docs # Generate Swagger docs + +# ๐Ÿ” Code quality +make lint # Linting +make format # Format code +``` + + +### Environment Configuration + +**๐Ÿ“ .env File:** + +```bash +# Database +BLUEPRINT_DB_HOST=localhost +BLUEPRINT_DB_PORT=5432 +BLUEPRINT_DB_USERNAME=postgres +BLUEPRINT_DB_PASSWORD=postgres +BLUEPRINT_DB_DATABASE=api_service + +# JWT +JWT_SECRET=your-super-secret-key-change-in-production + +# External APIs +BPJS_BASE_URL=https://api.bpjs-kesehatan.go.id +SATUSEHAT_BASE_URL=https://api.satusehat.kemkes.go.id + +# Application +APP_ENV=development +APP_PORT=8080 +LOG_LEVEL=debug +``` + + +*** + +## ๐Ÿš€ Deployment + +### ๐Ÿณ Docker Deployment + +**Development:** + +```bash +# Start semua services +make docker-run + +# Stop services +make docker-down + +# Rebuild dan restart +make docker-rebuild +``` + +**Production:** + +```bash +# Build production image +docker build -t api-service:prod . + +# Run production container +docker run -d \ + --name api-service \ + -p 8080:8080 \ + --env-file .env.prod \ + api-service:prod +``` + + +### ๐Ÿ”ง Manual Deployment + +```bash +# Build aplikasi +make build + +# Run migrations +./scripts/migrate.sh up + +# Start server +./bin/api-service +``` + + +*** + +## ๐Ÿ“š Dokumentasi + +### ๐Ÿ“– Interactive API Documentation + +Kunjungi **Swagger UI** di: http://localhost:8080/swagger/index.html + +**Cara menggunakan:** + +1. ๐Ÿ”‘ Login melalui `/auth/login` endpoint +2. ๐Ÿ“‹ Copy token dari response +3. ๐Ÿ”“ Klik tombol "Authorize" di Swagger +4. ๐Ÿ“ Masukkan: `Bearer ` +5. โœ… Test semua endpoint yang tersedia + +### ๐Ÿ”„ WebSocket Client Examples + +#### JavaScript WebSocket Client + +**Basic Connection:** + +```javascript +// Create WebSocket connection +const ws = new WebSocket('ws://localhost:8080/api/v1/ws'); + +// Connection opened +ws.onopen = function(event) { + console.log('Connected to WebSocket server'); +}; + +// Listen for messages +ws.onmessage = function(event) { + const data = JSON.parse(event.data); + console.log('Message received:', data); +}; + +// Send a message +function sendMessage(message) { + ws.send(JSON.stringify({ + type: 'broadcast', + message: message + })); +} + +// Handle connection close +ws.onclose = function(event) { + console.log('Disconnected from WebSocket server'); +}; +``` + +#### Vue.js Component Example + +```vue + + + +``` + +### ๐Ÿงช Testing Examples + +**WebSocket Testing with wscat:** + +```bash +# Install wscat globally +npm install -g wscat + +# Connect to WebSocket server +wscat -c ws://localhost:8080/api/v1/ws + +# Send a message +{"type": "broadcast", "message": "Hello from wscat!"} +``` + +**cURL for REST endpoints:** + +```bash +# Login +TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"password"}' | jq -r '.access_token') + +# Get WebSocket statistics +curl http://localhost:8080/api/websocket/stats + +# Broadcast message to all clients +curl -X POST http://localhost:8080/api/websocket/broadcast \ + -H "Content-Type: application/json" \ + -d '{"type": "broadcast", "message": "Server broadcast message"}' + +# Get system information +curl http://localhost:8080/api/sistem/info + +# Health check +curl http://localhost:8080/api/sistem/health +``` + +**Response Examples:** + +```json +{ + "connected_clients": 5, + "databases": ["default"], + "database_health": "connected", + "timestamp": 1694325600 +} +``` + + +*** + +## ๐Ÿšจ Troubleshooting + +### Masalah Umum + +**โŒ Database Connection Error** + +```bash +# Cek status PostgreSQL +make db-status + +# Reset database +make db-reset + +# Check logs +make logs-db +``` + +**โŒ Generate Handler Gagal** + +- โœ… Pastikan berada di root project +- โœ… Cek permission write di folder `internal/` +- โœ… Verifikasi file `internal/routes/v1/routes.go` exists + +**โŒ Token Invalid/Expired** + +- ๐Ÿ”„ Login ulang untuk mendapatkan token baru +- โฐ Token expire dalam 1 jam (configurable) +- ๐Ÿ“ Format harus: `Bearer ` + +**โŒ Import Error saat Generate** + +- ๐Ÿงน Jalankan: `go mod tidy` +- ๐Ÿ”„ Restart development server +- ๐Ÿ“ Cek format import di generated files + + +### Debug Mode + +```bash +# Enable debug logging +export LOG_LEVEL=debug + +# Run dengan verbose output +make run-debug + +# Monitor performance +make monitor +``` + + +*** + +## ๐ŸŽฏ Next Steps + +### ๐Ÿ“‹ WebSocket Development Roadmap + +- [x] โœ… **Setup environment selesai** +- [x] โœ… **Implementasi WebSocket server** +- [x] โœ… **Setup Vue.js client example** +- [x] โœ… **Test WebSocket functionality** +- [ ] ๐Ÿ”„ **Implementasi room management** +- [ ] ๐Ÿ”„ **Tambahkan authentication ke WebSocket** +- [ ] ๐Ÿ”„ **Implementasi database notifications** +- [ ] ๐Ÿ”„ **Tambahkan unit tests untuk WebSocket** +- [ ] ๐Ÿ”„ **Setup monitoring dan observability** +- [ ] ๐Ÿ”„ **Deploy ke production** + +### ๐Ÿš€ Advanced WebSocket Features + +- **๐Ÿ”„ Database Integration** - PostgreSQL LISTEN/NOTIFY +- **๐Ÿ“Š Real-time Analytics** - Connection metrics dan monitoring +- **๐Ÿ”’ Enhanced Security** - Rate limiting, CORS, authentication +- **๐Ÿ“ˆ Performance Optimization** - Connection pooling, message queuing +- **๐Ÿ  Multi-room Support** - Advanced room management +- **๐Ÿ“ฑ Mobile SDK Integration** - React Native, Flutter support +- **๐ŸŒ Multi-protocol Support** - MQTT, Server-Sent Events +- **๐Ÿ“ก Load Balancing** - Horizontal scaling untuk WebSocket + +### ๐Ÿ› ๏ธ Immediate Next Steps + +1. **Test WebSocket Connection** + ```bash + # Start the server + go run cmd/api/main.go + + # Test with wscat + wscat -c ws://localhost:8080/api/v1/ws + ``` + +2. **Try Vue.js Client Example** + ```bash + cd examples/clientsocket + npm install + npm run dev + ``` + +3. **Explore WebSocket Features** + - Join/leave rooms + - Send broadcast messages + - Direct messaging between clients + - Monitor connection statistics + +*** + +**โšก Total setup time: 5 menit | ๐Ÿ”„ WebSocket ready: Langsung test | ๐ŸŒ Client example: Vue.js included** + +> **๐Ÿ’ก Pro Tip:** Gunakan `make help` untuk melihat semua command yang tersedia + +*** + diff --git a/cmd/api/main.go b/cmd/api/main.go new file mode 100644 index 0000000..8f4e5c3 --- /dev/null +++ b/cmd/api/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os/signal" + "syscall" + "time" + + "api-service/internal/server" + + "github.com/joho/godotenv" // Import the godotenv package + + _ "api-service/docs" +) + +// @title API Service +// @version 1.0.0 +// @description A comprehensive Go API service with Swagger documentation +// @termsOfService http://swagger.io/terms/ + +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +// @host localhost:8080 +// @BasePath /api/v1 +// @schemes http https + +func gracefulShutdown(apiServer *http.Server, done chan bool) { + // Create context that listens for the interrupt signal from the OS. + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + // Listen for the interrupt signal. + <-ctx.Done() + + log.Println("shutting down gracefully, press Ctrl+C again to force") + stop() // Allow Ctrl+C to force shutdown + + // The context is used to inform the server it has 5 seconds to finish + // the request it is currently handling + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := apiServer.Shutdown(ctx); err != nil { + log.Printf("Server forced to shutdown with error: %v", err) + } + + log.Println("Server exiting") + + // Notify the main goroutine that the shutdown is complete + done <- true +} + +func main() { + log.Println("Starting API Service...") + + // Load environment variables from .env file + if err := godotenv.Load(); err != nil { + log.Printf("Warning: .env file not found or could not be loaded: %v", err) + log.Println("Continuing with system environment variables...") + } + + server := server.NewServer() + + // Create a done channel to signal when the shutdown is complete + done := make(chan bool, 1) + + // Run graceful shutdown in a separate goroutine + go gracefulShutdown(server, done) + + log.Printf("Server starting on port %s", server.Addr) + err := server.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + panic(fmt.Sprintf("http server error: %s", err)) + } + + // Wait for the graceful shutdown to complete + <-done + log.Println("Graceful shutdown complete.") +} diff --git a/cmd/logging/main.go b/cmd/logging/main.go new file mode 100644 index 0000000..97774de --- /dev/null +++ b/cmd/logging/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "fmt" + "log" + "time" + + "api-service/pkg/logger" +) + +func main() { + fmt.Println("Testing Dynamic Logging Functions...") + fmt.Println("====================================") + + // Test fungsi penyimpanan log dinamis + testDynamicLogging() + + // Tunggu sebentar untuk memastikan goroutine selesai + time.Sleep(500 * time.Millisecond) + + fmt.Println("\n====================================") + fmt.Println("Dynamic logging test completed!") + fmt.Println("Check the log files in pkg/logger/data/ directory") +} + +func testDynamicLogging() { + // Buat logger instance + loggerInstance := logger.New("test-app", logger.DEBUG, false) + + // Test 1: Log dengan penyimpanan otomatis + fmt.Println("\n1. Testing automatic log saving...") + loggerInstance.LogAndSave(logger.INFO, "Application started successfully", map[string]interface{}{ + "version": "1.0.0", + "build_date": time.Now().Format("2006-01-02"), + "environment": "development", + }) + + // Test 2: Log dengan request context + fmt.Println("\n2. Testing log with request context...") + requestLogger := loggerInstance.WithRequestID("req-001").WithCorrelationID("corr-001") + requestLogger.LogAndSave(logger.INFO, "User login attempt", map[string]interface{}{ + "username": "john_doe", + "ip": "192.168.1.100", + "success": true, + }) + + // Test 3: Error logging + fmt.Println("\n3. Testing error logging...") + loggerInstance.LogAndSave(logger.ERROR, "Database connection failed", map[string]interface{}{ + "error": "connection timeout", + "retry_count": 3, + "host": "db.example.com:5432", + }) + + // Test 4: Manual log entry saving + fmt.Println("\n4. Testing manual log entry saving...") + manualEntry := logger.LogEntry{ + Timestamp: time.Now().Format(time.RFC3339), + Level: "DEBUG", + Service: "manual-test", + Message: "Manual log entry created", + RequestID: "manual-req-001", + CorrelationID: "manual-corr-001", + File: "main.go", + Line: 42, + Fields: map[string]interface{}{ + "custom_field": "test_value", + "number": 123, + "active": true, + }, + } + + // Simpan manual ke berbagai format + if err := logger.SaveLogText(manualEntry); err != nil { + log.Printf("Error saving text log: %v", err) + } else { + fmt.Println("โœ“ Text log saved successfully") + } + + if err := logger.SaveLogJSON(manualEntry); err != nil { + log.Printf("Error saving JSON log: %v", err) + } else { + fmt.Println("โœ“ JSON log saved successfully") + } + + if err := logger.SaveLogToDatabase(manualEntry); err != nil { + log.Printf("Error saving database log: %v", err) + } else { + fmt.Println("โœ“ Database log saved successfully") + } + + // Test 5: Performance logging dengan durasi + fmt.Println("\n5. Testing performance logging...") + start := time.Now() + + // Simulasi proses yang memakan waktu + time.Sleep(200 * time.Millisecond) + + duration := time.Since(start) + loggerInstance.LogAndSave(logger.INFO, "Data processing completed", map[string]interface{}{ + "operation": "data_import", + "duration": duration.String(), + "duration_ms": duration.Milliseconds(), + "records": 1000, + "throughput": fmt.Sprintf("%.2f records/ms", 1000/float64(duration.Milliseconds())), + }) + + fmt.Println("\nโœ“ All logging tests completed successfully!") +} diff --git a/diagnostic/main.go b/diagnostic/main.go new file mode 100644 index 0000000..e1c5a2c --- /dev/null +++ b/diagnostic/main.go @@ -0,0 +1,130 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "os" + + _ "github.com/jackc/pgx/v5/stdlib" + "github.com/joho/godotenv" +) + +func main() { + fmt.Println("=== Database Connection Diagnostic Tool ===") + + // Load environment variables from .env file + if err := godotenv.Load(); err != nil { + log.Printf("Warning: Error loading .env file: %v", err) + } + + // Get configuration from environment + host := os.Getenv("DB_HOST") + port := os.Getenv("DB_PORT") + username := os.Getenv("DB_USERNAME") + password := os.Getenv("DB_PASSWORD") + database := os.Getenv("DB_DATABASE") + sslmode := os.Getenv("DB_SSLMODE") + + if sslmode == "" { + sslmode = "disable" + } + + fmt.Printf("Host: %s\n", host) + fmt.Printf("Port: %s\n", port) + fmt.Printf("Username: %s\n", username) + fmt.Printf("Database: %s\n", database) + fmt.Printf("SSL Mode: %s\n", sslmode) + + if host == "" || username == "" || password == "" { + fmt.Println("โŒ Missing required environment variables") + return + } + + // Test connection to PostgreSQL server + fmt.Println("\n--- Testing PostgreSQL Server Connection ---") + serverConnStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=postgres sslmode=%s", + host, port, username, password, sslmode) + + db, err := sql.Open("pgx", serverConnStr) + if err != nil { + fmt.Printf("โŒ Failed to connect to PostgreSQL server: %v\n", err) + return + } + defer db.Close() + + err = db.Ping() + if err != nil { + fmt.Printf("โŒ Failed to ping PostgreSQL server: %v\n", err) + return + } + + fmt.Println("โœ… Successfully connected to PostgreSQL server") + + // Check if database exists + fmt.Println("\n--- Checking Database Existence ---") + var exists bool + err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = $1)", database).Scan(&exists) + if err != nil { + fmt.Printf("โŒ Failed to check database existence: %v\n", err) + return + } + + if !exists { + fmt.Printf("โŒ Database '%s' does not exist\n", database) + + // List available databases + fmt.Println("\n--- Available Databases ---") + rows, err := db.Query("SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname") + if err != nil { + fmt.Printf("โŒ Failed to list databases: %v\n", err) + return + } + defer rows.Close() + + fmt.Println("Available databases:") + for rows.Next() { + var dbName string + if err := rows.Scan(&dbName); err != nil { + continue + } + fmt.Printf(" - %s\n", dbName) + } + return + } + + fmt.Printf("โœ… Database '%s' exists\n", database) + + // Test direct connection to the database + fmt.Println("\n--- Testing Direct Database Connection ---") + directConnStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", + host, port, username, password, database, sslmode) + + targetDB, err := sql.Open("pgx", directConnStr) + if err != nil { + fmt.Printf("โŒ Failed to connect to database '%s': %v\n", database, err) + return + } + defer targetDB.Close() + + err = targetDB.Ping() + if err != nil { + fmt.Printf("โŒ Failed to ping database '%s': %v\n", database, err) + return + } + + fmt.Printf("โœ… Successfully connected to database '%s'\n", database) + + // Test basic query + fmt.Println("\n--- Testing Basic Query ---") + var version string + err = targetDB.QueryRow("SELECT version()").Scan(&version) + if err != nil { + fmt.Printf("โŒ Failed to execute query: %v\n", err) + return + } + + fmt.Printf("โœ… PostgreSQL Version: %s\n", version) + + fmt.Println("\n๐ŸŽ‰ All tests passed! Database connection is working correctly.") +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..17b58e9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,194 @@ +services: + # # PostgreSQL Database + # psql_bp: + # image: postgres:15-alpine + # restart: unless-stopped + # environment: + # POSTGRES_USER: stim + # POSTGRES_PASSWORD: stim*RS54 + # POSTGRES_DB: satu_db + # ports: + # - "5432:5432" + # volumes: + # - postgres_data:/var/lib/postgresql/data + # healthcheck: + # test: ["CMD-SHELL", "pg_isready -U stim -d satu_db"] + # interval: 10s + # timeout: 5s + # retries: 5 + # networks: + # - blueprint + + # # MongoDB Database + # mongodb: + # image: mongo:7-jammy + # restart: unless-stopped + # environment: + # MONGO_INITDB_ROOT_USERNAME: admin + # MONGO_INITDB_ROOT_PASSWORD: stim*rs54 + # ports: + # - "27017:27017" + # volumes: + # - mongodb_data:/data/db + # networks: + # - blueprint + + # # MySQL Antrian Database + # mysql_antrian: + # image: mysql:8.0 + # restart: unless-stopped + # environment: + # MYSQL_ROOT_PASSWORD: www-data + # MYSQL_USER: www-data + # MYSQL_PASSWORD: www-data + # MYSQL_DATABASE: antrian_rssa + # ports: + # - "3306:3306" + # volumes: + # - mysql_antrian_data:/var/lib/mysql + # healthcheck: + # test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + # interval: 10s + # timeout: 5s + # retries: 5 + # networks: + # - blueprint + + # # MySQL Medical Database + # mysql_medical: + # image: mysql:8.0 + # restart: unless-stopped + # environment: + # MYSQL_ROOT_PASSWORD: meninjar*RS54 + # MYSQL_USER: meninjardev + # MYSQL_PASSWORD: meninjar*RS54 + # MYSQL_DATABASE: healtcare_database + # ports: + # - "3307:3306" + # volumes: + # - mysql_medical_data:/var/lib/mysql + # healthcheck: + # test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + # interval: 10s + # timeout: 5s + # retries: 5 + # networks: + # - blueprint + + # Main Application + app: + build: + context: . + dockerfile: Dockerfile + target: prod + restart: unless-stopped + ports: + - "8080:8080" + environment: + # Server Configuration + APP_ENV: production + PORT: 8080 + GIN_MODE: release + + # Default Database Configuration (PostgreSQL) + DB_CONNECTION: postgres + DB_USERNAME: stim + DB_PASSWORD: stim*RS54 + DB_HOST: 10.10.123.165 + DB_DATABASE: satu_db + DB_PORT: 5432 + DB_SSLMODE: disable + + # satudata Database Configuration (PostgreSQL) + POSTGRES_SATUDATA_CONNECTION: postgres + POSTGRES_SATUDATA_USERNAME: stim + POSTGRES_SATUDATA_PASSWORD: stim*RS54 + POSTGRES_SATUDATA_HOST: 10.10.123.165 + POSTGRES_SATUDATA_DATABASE: satu_db + POSTGRES_SATUDATA_PORT: 5432 + POSTGRES_SATUDATA_SSLMODE: disable + + # Mongo Database + MONGODB_MONGOHL7_CONNECTION: mongodb + MONGODB_MONGOHL7_HOST: 10.10.123.206 + MONGODB_MONGOHL7_PORT: 27017 + MONGODB_MONGOHL7_USER: admin + MONGODB_MONGOHL7_PASS: stim*rs54 + MONGODB_MONGOHL7_MASTER: master + MONGODB_MONGOHL7_LOCAL: local + MONGODB_MONGOHL7_SSLMODE: disable + + # MYSQL Antrian Database + # MYSQL_ANTRIAN_CONNECTION: mysql + # MYSQL_ANTRIAN_HOST: mysql_antrian + # MYSQL_ANTRIAN_USERNAME: www-data + # MYSQL_ANTRIAN_PASSWORD: www-data + # MYSQL_ANTRIAN_DATABASE: antrian_rssa + # MYSQL_ANTRIAN_PORT: 3306 + # MYSQL_ANTRIAN_SSLMODE: disable + + # MYSQL Medical Database + MYSQL_MEDICAL_CONNECTION: mysql + MYSQL_MEDICAL_HOST: 10.10.123.163 + MYSQL_MEDICAL_USERNAME: meninjardev + MYSQL_MEDICAL_PASSWORD: meninjar*RS54 + MYSQL_MEDICAL_DATABASE: healtcare_database + MYSQL_MEDICAL_PORT: 3306 + MYSQL_MEDICAL_SSLMODE: disable + + # Keycloak Configuration + KEYCLOAK_ISSUER: https://auth.rssa.top/realms/sandbox + KEYCLOAK_AUDIENCE: nuxtsim-pendaftaran + KEYCLOAK_JWKS_URL: https://auth.rssa.top/realms/sandbox/protocol/openid-connect/certs + KEYCLOAK_ENABLED: true + + # BPJS Configuration + BPJS_BASEURL: https://apijkn.bpjs-kesehatan.go.id/vclaim-rest + BPJS_CONSID: 5257 + BPJS_USERKEY: 4cf1cbef8c008440bbe9ef9ba789e482 + BPJS_SECRETKEY: 1bV363512D + + # SatuSehat Configuration + BRIDGING_SATUSEHAT_ORG_ID: 100026555 + BRIDGING_SATUSEHAT_FASYAKES_ID: 3573011 + BRIDGING_SATUSEHAT_CLIENT_ID: l1ZgJGW6K5pnrqGUikWM7fgIoquA2AQ5UUG0U8WqHaq2VEyZ + BRIDGING_SATUSEHAT_CLIENT_SECRET: Al3PTYAW6axPiAFwaFlpn8qShLFW5YGMgG8w1qhexgCc7lGTEjjcR6zxa06ThPDy + BRIDGING_SATUSEHAT_AUTH_URL: https://api-satusehat.kemkes.go.id/oauth2/v1 + BRIDGING_SATUSEHAT_BASE_URL: https://api-satusehat.kemkes.go.id/fhir-r4/v1 + BRIDGING_SATUSEHAT_CONSENT_URL: https://api-satusehat.dto.kemkes.go.id/consent/v1 + BRIDGING_SATUSEHAT_KFA_URL: https://api-satusehat.kemkes.go.id/kfa-v2 + + # Swagger Configuration + SWAGGER_TITLE: My Custom API Service + SWAGGER_DESCRIPTION: This is a custom API service for managing various resources + SWAGGER_VERSION: 2.0.0 + SWAGGER_CONTACT_NAME: Support Team + SWAGGER_HOST: api.mycompany.com:8080 + SWAGGER_BASE_PATH: /api/v2 + SWAGGER_SCHEMES: https + + # API Configuration + API_TITLE: API Service UJICOBA + API_DESCRIPTION: Dokumentation SWAGGER + API_VERSION: 3.0.0 + + # depends_on: + # psql_bp: + # condition: service_healthy + # mongodb: + # condition: service_started + # mysql_antrian: + # condition: service_healthy + # mysql_medical: + # condition: service_healthy + networks: + - goservice + +# volumes: +# postgres_data: +# mongodb_data: +# mysql_antrian_data: +# mysql_medical_data: + +networks: + goservice: diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..12f27b3 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,2238 @@ +// 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}}", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.swagger.io/support", + "email": "support@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/Peserta/nik/:nik": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get participant eligibility information by NIK", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Peserta" + ], + "summary": "Get Bynik data", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + }, + { + "type": "string", + "example": "\"example_value\"", + "description": "nik", + "name": "nik", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successfully retrieved Bynik data", + "schema": { + "$ref": "#/definitions/peserta.PesertaResponse" + } + }, + "400": { + "description": "Bad request - invalid parameters", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized - invalid API credentials", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "404": { + "description": "Not found - Bynik not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + } + }, + "/Peserta/nokartu/:nokartu": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get participant eligibility information by card number", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Peserta" + ], + "summary": "Get Bynokartu data", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + }, + { + "type": "string", + "example": "\"example_value\"", + "description": "nokartu", + "name": "nokartu", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successfully retrieved Bynokartu data", + "schema": { + "$ref": "#/definitions/peserta.PesertaResponse" + } + }, + "400": { + "description": "Bad request - invalid parameters", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized - invalid API credentials", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "404": { + "description": "Not found - Bynokartu not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + } + }, + "/Rujukan/:norujukan": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update existing Rujukan in BPJS system", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Rujukan" + ], + "summary": "Update existing Rujukan", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + }, + { + "description": "Rujukan update data", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rujukan.RujukanRequest" + } + } + ], + "responses": { + "200": { + "description": "Successfully updated Rujukan", + "schema": { + "$ref": "#/definitions/rujukan.RujukanResponse" + } + }, + "400": { + "description": "Bad request - invalid parameters", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized - invalid API credentials", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "404": { + "description": "Not found - Rujukan not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "409": { + "description": "Conflict - update conflict occurred", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Create new Rujukan in BPJS system", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Rujukan" + ], + "summary": "Create new Rujukan", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + }, + { + "description": "Rujukan data", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rujukan.RujukanRequest" + } + } + ], + "responses": { + "201": { + "description": "Successfully created Rujukan", + "schema": { + "$ref": "#/definitions/rujukan.RujukanResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete existing Rujukan from BPJS system", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Rujukan" + ], + "summary": "Delete existing Rujukan", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + } + ], + "responses": { + "200": { + "description": "Successfully deleted Rujukan", + "schema": { + "$ref": "#/definitions/rujukan.RujukanResponse" + } + }, + "400": { + "description": "Bad request - invalid parameters", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized - invalid API credentials", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "404": { + "description": "Not found - Rujukan not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + } + }, + "/Rujukanbalik/:norujukan": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update existing Rujukanbalik in BPJS system", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Rujukan" + ], + "summary": "Update existing Rujukanbalik", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + }, + { + "description": "Rujukanbalik update data", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rujukan.RujukanRequest" + } + } + ], + "responses": { + "200": { + "description": "Successfully updated Rujukanbalik", + "schema": { + "$ref": "#/definitions/rujukan.RujukanResponse" + } + }, + "400": { + "description": "Bad request - invalid parameters", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized - invalid API credentials", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "404": { + "description": "Not found - Rujukanbalik not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "409": { + "description": "Conflict - update conflict occurred", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Create new Rujukanbalik in BPJS system", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Rujukan" + ], + "summary": "Create new Rujukanbalik", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + }, + { + "description": "Rujukanbalik data", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rujukan.RujukanRequest" + } + } + ], + "responses": { + "201": { + "description": "Successfully created Rujukanbalik", + "schema": { + "$ref": "#/definitions/rujukan.RujukanResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete existing Rujukanbalik from BPJS system", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Rujukan" + ], + "summary": "Delete existing Rujukanbalik", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + } + ], + "responses": { + "200": { + "description": "Successfully deleted Rujukanbalik", + "schema": { + "$ref": "#/definitions/rujukan.RujukanResponse" + } + }, + "400": { + "description": "Bad request - invalid parameters", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized - invalid API credentials", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "404": { + "description": "Not found - Rujukanbalik not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + } + }, + "/api/v1/auth/login": { + "post": { + "description": "Authenticate user with username and password to receive JWT token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Login user and get JWT token", + "parameters": [ + { + "description": "Login credentials", + "name": "login", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.TokenResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/me": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get information about the currently authenticated user", + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Get current user info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/refresh": { + "post": { + "description": "Refresh the JWT token using a valid refresh token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Refresh JWT token", + "parameters": [ + { + "description": "Refresh token", + "name": "refresh", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.TokenResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/register": { + "post": { + "description": "Register a new user account", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Register new user", + "parameters": [ + { + "description": "Registration data", + "name": "register", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/retribusi/{id}": { + "get": { + "description": "Returns a single retribusi by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Retribusi" + ], + "summary": "Get Retribusi by ID", + "parameters": [ + { + "type": "string", + "description": "Retribusi ID (UUID)", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/retribusi.RetribusiGetByIDResponse" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Retribusi not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "put": { + "description": "Updates an existing retribusi record", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Retribusi" + ], + "summary": "Update retribusi", + "parameters": [ + { + "type": "string", + "description": "Retribusi ID (UUID)", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Retribusi update request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/retribusi.RetribusiUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "Retribusi updated successfully", + "schema": { + "$ref": "#/definitions/retribusi.RetribusiUpdateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Retribusi not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Soft deletes a retribusi by setting status to 'deleted'", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Retribusi" + ], + "summary": "Delete retribusi", + "parameters": [ + { + "type": "string", + "description": "Retribusi ID (UUID)", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Retribusi deleted successfully", + "schema": { + "$ref": "#/definitions/retribusi.RetribusiDeleteResponse" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Retribusi not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/v1/retribusis": { + "get": { + "description": "Returns a paginated list of retribusis with optional summary statistics", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Retribusi" + ], + "summary": "Get retribusi with pagination and optional aggregation", + "parameters": [ + { + "type": "integer", + "default": 10, + "description": "Limit (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "boolean", + "default": false, + "description": "Include aggregation summary", + "name": "include_summary", + "in": "query" + }, + { + "type": "string", + "description": "Filter by status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Filter by jenis", + "name": "jenis", + "in": "query" + }, + { + "type": "string", + "description": "Filter by dinas", + "name": "dinas", + "in": "query" + }, + { + "type": "string", + "description": "Search in multiple fields", + "name": "search", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/retribusi.RetribusiGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "post": { + "description": "Creates a new retribusi record", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Retribusi" + ], + "summary": "Create retribusi", + "parameters": [ + { + "description": "Retribusi creation request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/retribusi.RetribusiCreateRequest" + } + } + ], + "responses": { + "201": { + "description": "Retribusi created successfully", + "schema": { + "$ref": "#/definitions/retribusi.RetribusiCreateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/v1/retribusis/dynamic": { + "get": { + "description": "Returns retribusis with advanced dynamic filtering like Directus", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Retribusi" + ], + "summary": "Get retribusi with dynamic filtering", + "parameters": [ + { + "type": "string", + "description": "Fields to select (e.g., fields=*.*)", + "name": "fields", + "in": "query" + }, + { + "type": "string", + "description": "Dynamic filters (e.g., filter[Jenis][_eq]=value)", + "name": "filter[column][operator]", + "in": "query" + }, + { + "type": "string", + "description": "Sort fields (e.g., sort=date_created,-Jenis)", + "name": "sort", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/retribusi.RetribusiGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/v1/retribusis/stats": { + "get": { + "description": "Returns comprehensive statistics about retribusi data", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Retribusi" + ], + "summary": "Get retribusi statistics", + "parameters": [ + { + "type": "string", + "description": "Filter statistics by status", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Statistics data", + "schema": { + "$ref": "#/definitions/models.AggregateData" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/v1/token/generate": { + "post": { + "description": "Generate a JWT token for a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Token" + ], + "summary": "Generate JWT token", + "parameters": [ + { + "description": "User credentials", + "name": "token", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.TokenResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/token/generate-direct": { + "post": { + "description": "Generate a JWT token directly without password verification (for testing)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Token" + ], + "summary": "Generate token directly", + "parameters": [ + { + "description": "User info", + "name": "user", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.TokenResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/bynokartu/:nokartu": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get rujukan by card number", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Rujukan" + ], + "summary": "Get Bynokartu data", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + }, + { + "type": "string", + "example": "\"example_value\"", + "description": "nokartu", + "name": "nokartu", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successfully retrieved Bynokartu data", + "schema": { + "$ref": "#/definitions/rujukan.RujukanResponse" + } + }, + "400": { + "description": "Bad request - invalid parameters", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized - invalid API credentials", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "404": { + "description": "Not found - Bynokartu not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + } + }, + "/bynorujukan/:norujukan": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get rujukan by nomor rujukan", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Rujukan" + ], + "summary": "Get Bynorujukan data", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + }, + { + "type": "string", + "example": "\"example_value\"", + "description": "norujukan", + "name": "norujukan", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successfully retrieved Bynorujukan data", + "schema": { + "$ref": "#/definitions/rujukan.RujukanResponse" + } + }, + "400": { + "description": "Bad request - invalid parameters", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized - invalid API credentials", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "404": { + "description": "Not found - Bynorujukan not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + } + } + }, + "definitions": { + "models.AggregateData": { + "type": "object", + "properties": { + "by_dinas": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "by_jenis": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "by_status": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "created_today": { + "type": "integer" + }, + "last_updated": { + "type": "string" + }, + "total_active": { + "type": "integer" + }, + "total_draft": { + "type": "integer" + }, + "total_inactive": { + "type": "integer" + }, + "updated_today": { + "type": "integer" + } + } + }, + "models.ErrorResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "models.ErrorResponseBpjs": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "errors": { + "type": "object", + "additionalProperties": true + }, + "message": { + "type": "string" + }, + "request_id": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "models.LoginRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "models.MetaResponse": { + "type": "object", + "properties": { + "current_page": { + "type": "integer" + }, + "has_next": { + "type": "boolean" + }, + "has_prev": { + "type": "boolean" + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "total_pages": { + "type": "integer" + } + } + }, + "models.NullableInt32": { + "type": "object", + "properties": { + "int32": { + "type": "integer" + }, + "valid": { + "type": "boolean" + } + } + }, + "models.NullableString": { + "type": "object", + "properties": { + "string": { + "type": "string" + }, + "valid": { + "type": "boolean" + } + } + }, + "models.NullableTime": { + "type": "object", + "properties": { + "time": { + "type": "string" + }, + "valid": { + "type": "boolean" + } + } + }, + "models.TokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "token_type": { + "type": "string" + } + } + }, + "models.User": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "role": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "peserta.PesertaData": { + "type": "object", + "properties": { + "cob": { + "type": "object", + "properties": { + "nmAsuransi": {}, + "noAsuransi": {}, + "tglTAT": {}, + "tglTMT": {} + } + }, + "hakKelas": { + "type": "object", + "properties": { + "keterangan": { + "type": "string" + }, + "kode": { + "type": "string" + } + } + }, + "informasi": { + "type": "object", + "properties": { + "dinsos": {}, + "eSEP": {}, + "noSKTM": {}, + "prolanisPRB": { + "type": "string" + } + } + }, + "jenisPeserta": { + "type": "object", + "properties": { + "keterangan": { + "type": "string" + }, + "kode": { + "type": "string" + } + } + }, + "mr": { + "type": "object", + "properties": { + "noMR": { + "type": "string" + }, + "noTelepon": { + "type": "string" + } + } + }, + "nama": { + "type": "string" + }, + "nik": { + "type": "string" + }, + "noKartu": { + "type": "string" + }, + "pisa": { + "type": "string" + }, + "provUmum": { + "type": "object", + "properties": { + "kdProvider": { + "type": "string" + }, + "nmProvider": { + "type": "string" + } + } + }, + "raw_response": { + "type": "string" + }, + "sex": { + "type": "string" + }, + "statusPeserta": { + "type": "object", + "properties": { + "keterangan": { + "type": "string" + }, + "kode": { + "type": "string" + } + } + }, + "tglCetakKartu": { + "type": "string" + }, + "tglLahir": { + "type": "string" + }, + "tglTAT": { + "type": "string" + }, + "tglTMT": { + "type": "string" + }, + "umur": { + "type": "object", + "properties": { + "umurSaatPelayanan": { + "type": "string" + }, + "umurSekarang": { + "type": "string" + } + } + } + } + }, + "peserta.PesertaResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/peserta.PesertaData" + }, + "message": { + "type": "string" + }, + "metaData": {}, + "request_id": { + "type": "string" + }, + "status": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "retribusi.Retribusi": { + "type": "object", + "properties": { + "date_created": { + "$ref": "#/definitions/models.NullableTime" + }, + "date_updated": { + "$ref": "#/definitions/models.NullableTime" + }, + "dinas": { + "$ref": "#/definitions/models.NullableString" + }, + "id": { + "type": "string" + }, + "jenis": { + "$ref": "#/definitions/models.NullableString" + }, + "kelompok_obyek": { + "$ref": "#/definitions/models.NullableString" + }, + "kode_tarif": { + "$ref": "#/definitions/models.NullableString" + }, + "pelayanan": { + "$ref": "#/definitions/models.NullableString" + }, + "rekening_denda": { + "$ref": "#/definitions/models.NullableString" + }, + "rekening_pokok": { + "$ref": "#/definitions/models.NullableString" + }, + "satuan": { + "$ref": "#/definitions/models.NullableString" + }, + "satuan_overtime": { + "$ref": "#/definitions/models.NullableString" + }, + "sort": { + "$ref": "#/definitions/models.NullableInt32" + }, + "status": { + "type": "string" + }, + "tarif": { + "$ref": "#/definitions/models.NullableString" + }, + "tarif_overtime": { + "$ref": "#/definitions/models.NullableString" + }, + "uraian_1": { + "$ref": "#/definitions/models.NullableString" + }, + "uraian_2": { + "$ref": "#/definitions/models.NullableString" + }, + "uraian_3": { + "$ref": "#/definitions/models.NullableString" + }, + "user_created": { + "$ref": "#/definitions/models.NullableString" + }, + "user_updated": { + "$ref": "#/definitions/models.NullableString" + } + } + }, + "retribusi.RetribusiCreateRequest": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "dinas": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "jenis": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "kelompok_obyek": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "kode_tarif": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "pelayanan": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "rekening_denda": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "rekening_pokok": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "satuan": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "satuan_overtime": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "status": { + "type": "string", + "enum": [ + "draft", + "active", + "inactive" + ] + }, + "tarif": { + "type": "string" + }, + "tarif_overtime": { + "type": "string" + }, + "uraian_1": { + "type": "string" + }, + "uraian_2": { + "type": "string" + }, + "uraian_3": { + "type": "string" + } + } + }, + "retribusi.RetribusiCreateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/retribusi.Retribusi" + }, + "message": { + "type": "string" + } + } + }, + "retribusi.RetribusiDeleteResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "retribusi.RetribusiGetByIDResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/retribusi.Retribusi" + }, + "message": { + "type": "string" + } + } + }, + "retribusi.RetribusiGetResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/retribusi.Retribusi" + } + }, + "message": { + "type": "string" + }, + "meta": { + "$ref": "#/definitions/models.MetaResponse" + }, + "summary": { + "$ref": "#/definitions/models.AggregateData" + } + } + }, + "retribusi.RetribusiUpdateRequest": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "dinas": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "jenis": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "kelompok_obyek": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "kode_tarif": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "pelayanan": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "rekening_denda": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "rekening_pokok": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "satuan": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "satuan_overtime": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "status": { + "type": "string", + "enum": [ + "draft", + "active", + "inactive" + ] + }, + "tarif": { + "type": "string" + }, + "tarif_overtime": { + "type": "string" + }, + "uraian_1": { + "type": "string" + }, + "uraian_2": { + "type": "string" + }, + "uraian_3": { + "type": "string" + } + } + }, + "retribusi.RetribusiUpdateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/retribusi.Retribusi" + }, + "message": { + "type": "string" + } + } + }, + "rujukan.DataPeserta": { + "type": "object", + "properties": { + "cob": { + "type": "object", + "properties": { + "nmAsuransi": {}, + "noAsuransi": {}, + "tglTAT": {}, + "tglTMT": {} + } + }, + "hakKelas": { + "type": "object", + "properties": { + "keterangan": { + "type": "string" + }, + "kode": { + "type": "string" + } + } + }, + "informasi": { + "type": "object", + "properties": { + "dinsos": {}, + "noSKTM": {}, + "prolanisPRB": {} + } + }, + "jenisPeserta": { + "type": "object", + "properties": { + "keterangan": { + "type": "string" + }, + "kode": { + "type": "string" + } + } + }, + "mr": { + "type": "object", + "properties": { + "noMR": { + "type": "string" + }, + "noTelepon": {} + } + }, + "nama": { + "type": "string" + }, + "nik": { + "type": "string" + }, + "noKartu": { + "type": "string" + }, + "pisa": { + "type": "string" + }, + "provUmum": { + "type": "object", + "properties": { + "kdProvider": { + "type": "string" + }, + "nmProvider": { + "type": "string" + } + } + }, + "sex": { + "type": "string" + }, + "statusPeserta": { + "type": "object", + "properties": { + "keterangan": { + "type": "string" + }, + "kode": { + "type": "string" + } + } + }, + "tglCetakKartu": { + "type": "string" + }, + "tglLahir": { + "type": "string" + }, + "tglTAT": { + "type": "string" + }, + "tglTMT": { + "type": "string" + }, + "umur": { + "type": "object", + "properties": { + "umurSaatPelayanan": { + "type": "string" + }, + "umurSekarang": { + "type": "string" + } + } + } + } + }, + "rujukan.DiagnosaData": { + "type": "object", + "properties": { + "kode": { + "type": "string" + }, + "nama": { + "type": "string" + } + } + }, + "rujukan.PelayananData": { + "type": "object", + "properties": { + "kode": { + "type": "string" + }, + "nama": { + "type": "string" + } + } + }, + "rujukan.PoliRujukanData": { + "type": "object", + "properties": { + "kode": { + "type": "string" + }, + "nama": { + "type": "string" + } + } + }, + "rujukan.ProvPerujukData": { + "type": "object", + "properties": { + "kode": { + "type": "string" + }, + "nama": { + "type": "string" + } + } + }, + "rujukan.RujukanData": { + "type": "object", + "properties": { + "diagnosa": { + "$ref": "#/definitions/rujukan.DiagnosaData" + }, + "keluhan": { + "type": "string" + }, + "noKunjungan": { + "type": "string" + }, + "pelayanan": { + "$ref": "#/definitions/rujukan.PelayananData" + }, + "peserta": { + "$ref": "#/definitions/rujukan.DataPeserta" + }, + "poliRujukan": { + "$ref": "#/definitions/rujukan.PoliRujukanData" + }, + "provPerujuk": { + "$ref": "#/definitions/rujukan.ProvPerujukData" + }, + "tglKunjungan": { + "type": "string" + } + } + }, + "rujukan.RujukanRequest": { + "type": "object", + "required": [ + "noRujukan" + ], + "properties": { + "noKartu": { + "type": "string" + }, + "noRujukan": { + "type": "string" + }, + "request_id": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "rujukan.RujukanResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rujukan.RujukanData" + }, + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/rujukan.RujukanData" + } + }, + "message": { + "type": "string" + }, + "metaData": {}, + "request_id": { + "type": "string" + }, + "status": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0.0", + Host: "localhost:8080", + BasePath: "/api/v1", + Schemes: []string{"http", "https"}, + Title: "API Service", + Description: "A comprehensive Go API service with Swagger documentation", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..47b3b1c --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,2218 @@ +{ + "schemes": [ + "http", + "https" + ], + "swagger": "2.0", + "info": { + "description": "A comprehensive Go API service with Swagger documentation", + "title": "API Service", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.swagger.io/support", + "email": "support@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.0" + }, + "host": "localhost:8080", + "basePath": "/api/v1", + "paths": { + "/Peserta/nik/:nik": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get participant eligibility information by NIK", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Peserta" + ], + "summary": "Get Bynik data", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + }, + { + "type": "string", + "example": "\"example_value\"", + "description": "nik", + "name": "nik", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successfully retrieved Bynik data", + "schema": { + "$ref": "#/definitions/peserta.PesertaResponse" + } + }, + "400": { + "description": "Bad request - invalid parameters", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized - invalid API credentials", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "404": { + "description": "Not found - Bynik not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + } + }, + "/Peserta/nokartu/:nokartu": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get participant eligibility information by card number", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Peserta" + ], + "summary": "Get Bynokartu data", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + }, + { + "type": "string", + "example": "\"example_value\"", + "description": "nokartu", + "name": "nokartu", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successfully retrieved Bynokartu data", + "schema": { + "$ref": "#/definitions/peserta.PesertaResponse" + } + }, + "400": { + "description": "Bad request - invalid parameters", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized - invalid API credentials", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "404": { + "description": "Not found - Bynokartu not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + } + }, + "/Rujukan/:norujukan": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update existing Rujukan in BPJS system", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Rujukan" + ], + "summary": "Update existing Rujukan", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + }, + { + "description": "Rujukan update data", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rujukan.RujukanRequest" + } + } + ], + "responses": { + "200": { + "description": "Successfully updated Rujukan", + "schema": { + "$ref": "#/definitions/rujukan.RujukanResponse" + } + }, + "400": { + "description": "Bad request - invalid parameters", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized - invalid API credentials", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "404": { + "description": "Not found - Rujukan not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "409": { + "description": "Conflict - update conflict occurred", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Create new Rujukan in BPJS system", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Rujukan" + ], + "summary": "Create new Rujukan", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + }, + { + "description": "Rujukan data", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rujukan.RujukanRequest" + } + } + ], + "responses": { + "201": { + "description": "Successfully created Rujukan", + "schema": { + "$ref": "#/definitions/rujukan.RujukanResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete existing Rujukan from BPJS system", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Rujukan" + ], + "summary": "Delete existing Rujukan", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + } + ], + "responses": { + "200": { + "description": "Successfully deleted Rujukan", + "schema": { + "$ref": "#/definitions/rujukan.RujukanResponse" + } + }, + "400": { + "description": "Bad request - invalid parameters", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized - invalid API credentials", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "404": { + "description": "Not found - Rujukan not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + } + }, + "/Rujukanbalik/:norujukan": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update existing Rujukanbalik in BPJS system", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Rujukan" + ], + "summary": "Update existing Rujukanbalik", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + }, + { + "description": "Rujukanbalik update data", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rujukan.RujukanRequest" + } + } + ], + "responses": { + "200": { + "description": "Successfully updated Rujukanbalik", + "schema": { + "$ref": "#/definitions/rujukan.RujukanResponse" + } + }, + "400": { + "description": "Bad request - invalid parameters", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized - invalid API credentials", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "404": { + "description": "Not found - Rujukanbalik not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "409": { + "description": "Conflict - update conflict occurred", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Create new Rujukanbalik in BPJS system", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Rujukan" + ], + "summary": "Create new Rujukanbalik", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + }, + { + "description": "Rujukanbalik data", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rujukan.RujukanRequest" + } + } + ], + "responses": { + "201": { + "description": "Successfully created Rujukanbalik", + "schema": { + "$ref": "#/definitions/rujukan.RujukanResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete existing Rujukanbalik from BPJS system", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Rujukan" + ], + "summary": "Delete existing Rujukanbalik", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + } + ], + "responses": { + "200": { + "description": "Successfully deleted Rujukanbalik", + "schema": { + "$ref": "#/definitions/rujukan.RujukanResponse" + } + }, + "400": { + "description": "Bad request - invalid parameters", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized - invalid API credentials", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "404": { + "description": "Not found - Rujukanbalik not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + } + }, + "/api/v1/auth/login": { + "post": { + "description": "Authenticate user with username and password to receive JWT token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Login user and get JWT token", + "parameters": [ + { + "description": "Login credentials", + "name": "login", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.TokenResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/me": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get information about the currently authenticated user", + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Get current user info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/refresh": { + "post": { + "description": "Refresh the JWT token using a valid refresh token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Refresh JWT token", + "parameters": [ + { + "description": "Refresh token", + "name": "refresh", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.TokenResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/register": { + "post": { + "description": "Register a new user account", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Register new user", + "parameters": [ + { + "description": "Registration data", + "name": "register", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/retribusi/{id}": { + "get": { + "description": "Returns a single retribusi by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Retribusi" + ], + "summary": "Get Retribusi by ID", + "parameters": [ + { + "type": "string", + "description": "Retribusi ID (UUID)", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/retribusi.RetribusiGetByIDResponse" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Retribusi not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "put": { + "description": "Updates an existing retribusi record", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Retribusi" + ], + "summary": "Update retribusi", + "parameters": [ + { + "type": "string", + "description": "Retribusi ID (UUID)", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Retribusi update request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/retribusi.RetribusiUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "Retribusi updated successfully", + "schema": { + "$ref": "#/definitions/retribusi.RetribusiUpdateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Retribusi not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Soft deletes a retribusi by setting status to 'deleted'", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Retribusi" + ], + "summary": "Delete retribusi", + "parameters": [ + { + "type": "string", + "description": "Retribusi ID (UUID)", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Retribusi deleted successfully", + "schema": { + "$ref": "#/definitions/retribusi.RetribusiDeleteResponse" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Retribusi not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/v1/retribusis": { + "get": { + "description": "Returns a paginated list of retribusis with optional summary statistics", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Retribusi" + ], + "summary": "Get retribusi with pagination and optional aggregation", + "parameters": [ + { + "type": "integer", + "default": 10, + "description": "Limit (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "boolean", + "default": false, + "description": "Include aggregation summary", + "name": "include_summary", + "in": "query" + }, + { + "type": "string", + "description": "Filter by status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Filter by jenis", + "name": "jenis", + "in": "query" + }, + { + "type": "string", + "description": "Filter by dinas", + "name": "dinas", + "in": "query" + }, + { + "type": "string", + "description": "Search in multiple fields", + "name": "search", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/retribusi.RetribusiGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "post": { + "description": "Creates a new retribusi record", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Retribusi" + ], + "summary": "Create retribusi", + "parameters": [ + { + "description": "Retribusi creation request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/retribusi.RetribusiCreateRequest" + } + } + ], + "responses": { + "201": { + "description": "Retribusi created successfully", + "schema": { + "$ref": "#/definitions/retribusi.RetribusiCreateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/v1/retribusis/dynamic": { + "get": { + "description": "Returns retribusis with advanced dynamic filtering like Directus", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Retribusi" + ], + "summary": "Get retribusi with dynamic filtering", + "parameters": [ + { + "type": "string", + "description": "Fields to select (e.g., fields=*.*)", + "name": "fields", + "in": "query" + }, + { + "type": "string", + "description": "Dynamic filters (e.g., filter[Jenis][_eq]=value)", + "name": "filter[column][operator]", + "in": "query" + }, + { + "type": "string", + "description": "Sort fields (e.g., sort=date_created,-Jenis)", + "name": "sort", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/retribusi.RetribusiGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/v1/retribusis/stats": { + "get": { + "description": "Returns comprehensive statistics about retribusi data", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Retribusi" + ], + "summary": "Get retribusi statistics", + "parameters": [ + { + "type": "string", + "description": "Filter statistics by status", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Statistics data", + "schema": { + "$ref": "#/definitions/models.AggregateData" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/v1/token/generate": { + "post": { + "description": "Generate a JWT token for a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Token" + ], + "summary": "Generate JWT token", + "parameters": [ + { + "description": "User credentials", + "name": "token", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.TokenResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/token/generate-direct": { + "post": { + "description": "Generate a JWT token directly without password verification (for testing)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Token" + ], + "summary": "Generate token directly", + "parameters": [ + { + "description": "User info", + "name": "user", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.TokenResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/bynokartu/:nokartu": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get rujukan by card number", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Rujukan" + ], + "summary": "Get Bynokartu data", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + }, + { + "type": "string", + "example": "\"example_value\"", + "description": "nokartu", + "name": "nokartu", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successfully retrieved Bynokartu data", + "schema": { + "$ref": "#/definitions/rujukan.RujukanResponse" + } + }, + "400": { + "description": "Bad request - invalid parameters", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized - invalid API credentials", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "404": { + "description": "Not found - Bynokartu not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + } + }, + "/bynorujukan/:norujukan": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get rujukan by nomor rujukan", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Rujukan" + ], + "summary": "Get Bynorujukan data", + "parameters": [ + { + "type": "string", + "description": "Request ID for tracking", + "name": "X-Request-ID", + "in": "header" + }, + { + "type": "string", + "example": "\"example_value\"", + "description": "norujukan", + "name": "norujukan", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successfully retrieved Bynorujukan data", + "schema": { + "$ref": "#/definitions/rujukan.RujukanResponse" + } + }, + "400": { + "description": "Bad request - invalid parameters", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "401": { + "description": "Unauthorized - invalid API credentials", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "404": { + "description": "Not found - Bynorujukan not found", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.ErrorResponseBpjs" + } + } + } + } + } + }, + "definitions": { + "models.AggregateData": { + "type": "object", + "properties": { + "by_dinas": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "by_jenis": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "by_status": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "created_today": { + "type": "integer" + }, + "last_updated": { + "type": "string" + }, + "total_active": { + "type": "integer" + }, + "total_draft": { + "type": "integer" + }, + "total_inactive": { + "type": "integer" + }, + "updated_today": { + "type": "integer" + } + } + }, + "models.ErrorResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "models.ErrorResponseBpjs": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "errors": { + "type": "object", + "additionalProperties": true + }, + "message": { + "type": "string" + }, + "request_id": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "models.LoginRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "models.MetaResponse": { + "type": "object", + "properties": { + "current_page": { + "type": "integer" + }, + "has_next": { + "type": "boolean" + }, + "has_prev": { + "type": "boolean" + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "total_pages": { + "type": "integer" + } + } + }, + "models.NullableInt32": { + "type": "object", + "properties": { + "int32": { + "type": "integer" + }, + "valid": { + "type": "boolean" + } + } + }, + "models.NullableString": { + "type": "object", + "properties": { + "string": { + "type": "string" + }, + "valid": { + "type": "boolean" + } + } + }, + "models.NullableTime": { + "type": "object", + "properties": { + "time": { + "type": "string" + }, + "valid": { + "type": "boolean" + } + } + }, + "models.TokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "token_type": { + "type": "string" + } + } + }, + "models.User": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "role": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "peserta.PesertaData": { + "type": "object", + "properties": { + "cob": { + "type": "object", + "properties": { + "nmAsuransi": {}, + "noAsuransi": {}, + "tglTAT": {}, + "tglTMT": {} + } + }, + "hakKelas": { + "type": "object", + "properties": { + "keterangan": { + "type": "string" + }, + "kode": { + "type": "string" + } + } + }, + "informasi": { + "type": "object", + "properties": { + "dinsos": {}, + "eSEP": {}, + "noSKTM": {}, + "prolanisPRB": { + "type": "string" + } + } + }, + "jenisPeserta": { + "type": "object", + "properties": { + "keterangan": { + "type": "string" + }, + "kode": { + "type": "string" + } + } + }, + "mr": { + "type": "object", + "properties": { + "noMR": { + "type": "string" + }, + "noTelepon": { + "type": "string" + } + } + }, + "nama": { + "type": "string" + }, + "nik": { + "type": "string" + }, + "noKartu": { + "type": "string" + }, + "pisa": { + "type": "string" + }, + "provUmum": { + "type": "object", + "properties": { + "kdProvider": { + "type": "string" + }, + "nmProvider": { + "type": "string" + } + } + }, + "raw_response": { + "type": "string" + }, + "sex": { + "type": "string" + }, + "statusPeserta": { + "type": "object", + "properties": { + "keterangan": { + "type": "string" + }, + "kode": { + "type": "string" + } + } + }, + "tglCetakKartu": { + "type": "string" + }, + "tglLahir": { + "type": "string" + }, + "tglTAT": { + "type": "string" + }, + "tglTMT": { + "type": "string" + }, + "umur": { + "type": "object", + "properties": { + "umurSaatPelayanan": { + "type": "string" + }, + "umurSekarang": { + "type": "string" + } + } + } + } + }, + "peserta.PesertaResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/peserta.PesertaData" + }, + "message": { + "type": "string" + }, + "metaData": {}, + "request_id": { + "type": "string" + }, + "status": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "retribusi.Retribusi": { + "type": "object", + "properties": { + "date_created": { + "$ref": "#/definitions/models.NullableTime" + }, + "date_updated": { + "$ref": "#/definitions/models.NullableTime" + }, + "dinas": { + "$ref": "#/definitions/models.NullableString" + }, + "id": { + "type": "string" + }, + "jenis": { + "$ref": "#/definitions/models.NullableString" + }, + "kelompok_obyek": { + "$ref": "#/definitions/models.NullableString" + }, + "kode_tarif": { + "$ref": "#/definitions/models.NullableString" + }, + "pelayanan": { + "$ref": "#/definitions/models.NullableString" + }, + "rekening_denda": { + "$ref": "#/definitions/models.NullableString" + }, + "rekening_pokok": { + "$ref": "#/definitions/models.NullableString" + }, + "satuan": { + "$ref": "#/definitions/models.NullableString" + }, + "satuan_overtime": { + "$ref": "#/definitions/models.NullableString" + }, + "sort": { + "$ref": "#/definitions/models.NullableInt32" + }, + "status": { + "type": "string" + }, + "tarif": { + "$ref": "#/definitions/models.NullableString" + }, + "tarif_overtime": { + "$ref": "#/definitions/models.NullableString" + }, + "uraian_1": { + "$ref": "#/definitions/models.NullableString" + }, + "uraian_2": { + "$ref": "#/definitions/models.NullableString" + }, + "uraian_3": { + "$ref": "#/definitions/models.NullableString" + }, + "user_created": { + "$ref": "#/definitions/models.NullableString" + }, + "user_updated": { + "$ref": "#/definitions/models.NullableString" + } + } + }, + "retribusi.RetribusiCreateRequest": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "dinas": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "jenis": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "kelompok_obyek": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "kode_tarif": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "pelayanan": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "rekening_denda": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "rekening_pokok": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "satuan": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "satuan_overtime": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "status": { + "type": "string", + "enum": [ + "draft", + "active", + "inactive" + ] + }, + "tarif": { + "type": "string" + }, + "tarif_overtime": { + "type": "string" + }, + "uraian_1": { + "type": "string" + }, + "uraian_2": { + "type": "string" + }, + "uraian_3": { + "type": "string" + } + } + }, + "retribusi.RetribusiCreateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/retribusi.Retribusi" + }, + "message": { + "type": "string" + } + } + }, + "retribusi.RetribusiDeleteResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "retribusi.RetribusiGetByIDResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/retribusi.Retribusi" + }, + "message": { + "type": "string" + } + } + }, + "retribusi.RetribusiGetResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/retribusi.Retribusi" + } + }, + "message": { + "type": "string" + }, + "meta": { + "$ref": "#/definitions/models.MetaResponse" + }, + "summary": { + "$ref": "#/definitions/models.AggregateData" + } + } + }, + "retribusi.RetribusiUpdateRequest": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "dinas": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "jenis": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "kelompok_obyek": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "kode_tarif": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "pelayanan": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "rekening_denda": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "rekening_pokok": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "satuan": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "satuan_overtime": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "status": { + "type": "string", + "enum": [ + "draft", + "active", + "inactive" + ] + }, + "tarif": { + "type": "string" + }, + "tarif_overtime": { + "type": "string" + }, + "uraian_1": { + "type": "string" + }, + "uraian_2": { + "type": "string" + }, + "uraian_3": { + "type": "string" + } + } + }, + "retribusi.RetribusiUpdateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/retribusi.Retribusi" + }, + "message": { + "type": "string" + } + } + }, + "rujukan.DataPeserta": { + "type": "object", + "properties": { + "cob": { + "type": "object", + "properties": { + "nmAsuransi": {}, + "noAsuransi": {}, + "tglTAT": {}, + "tglTMT": {} + } + }, + "hakKelas": { + "type": "object", + "properties": { + "keterangan": { + "type": "string" + }, + "kode": { + "type": "string" + } + } + }, + "informasi": { + "type": "object", + "properties": { + "dinsos": {}, + "noSKTM": {}, + "prolanisPRB": {} + } + }, + "jenisPeserta": { + "type": "object", + "properties": { + "keterangan": { + "type": "string" + }, + "kode": { + "type": "string" + } + } + }, + "mr": { + "type": "object", + "properties": { + "noMR": { + "type": "string" + }, + "noTelepon": {} + } + }, + "nama": { + "type": "string" + }, + "nik": { + "type": "string" + }, + "noKartu": { + "type": "string" + }, + "pisa": { + "type": "string" + }, + "provUmum": { + "type": "object", + "properties": { + "kdProvider": { + "type": "string" + }, + "nmProvider": { + "type": "string" + } + } + }, + "sex": { + "type": "string" + }, + "statusPeserta": { + "type": "object", + "properties": { + "keterangan": { + "type": "string" + }, + "kode": { + "type": "string" + } + } + }, + "tglCetakKartu": { + "type": "string" + }, + "tglLahir": { + "type": "string" + }, + "tglTAT": { + "type": "string" + }, + "tglTMT": { + "type": "string" + }, + "umur": { + "type": "object", + "properties": { + "umurSaatPelayanan": { + "type": "string" + }, + "umurSekarang": { + "type": "string" + } + } + } + } + }, + "rujukan.DiagnosaData": { + "type": "object", + "properties": { + "kode": { + "type": "string" + }, + "nama": { + "type": "string" + } + } + }, + "rujukan.PelayananData": { + "type": "object", + "properties": { + "kode": { + "type": "string" + }, + "nama": { + "type": "string" + } + } + }, + "rujukan.PoliRujukanData": { + "type": "object", + "properties": { + "kode": { + "type": "string" + }, + "nama": { + "type": "string" + } + } + }, + "rujukan.ProvPerujukData": { + "type": "object", + "properties": { + "kode": { + "type": "string" + }, + "nama": { + "type": "string" + } + } + }, + "rujukan.RujukanData": { + "type": "object", + "properties": { + "diagnosa": { + "$ref": "#/definitions/rujukan.DiagnosaData" + }, + "keluhan": { + "type": "string" + }, + "noKunjungan": { + "type": "string" + }, + "pelayanan": { + "$ref": "#/definitions/rujukan.PelayananData" + }, + "peserta": { + "$ref": "#/definitions/rujukan.DataPeserta" + }, + "poliRujukan": { + "$ref": "#/definitions/rujukan.PoliRujukanData" + }, + "provPerujuk": { + "$ref": "#/definitions/rujukan.ProvPerujukData" + }, + "tglKunjungan": { + "type": "string" + } + } + }, + "rujukan.RujukanRequest": { + "type": "object", + "required": [ + "noRujukan" + ], + "properties": { + "noKartu": { + "type": "string" + }, + "noRujukan": { + "type": "string" + }, + "request_id": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "rujukan.RujukanResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rujukan.RujukanData" + }, + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/rujukan.RujukanData" + } + }, + "message": { + "type": "string" + }, + "metaData": {}, + "request_id": { + "type": "string" + }, + "status": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..84640d0 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,1464 @@ +basePath: /api/v1 +definitions: + models.AggregateData: + properties: + by_dinas: + additionalProperties: + type: integer + type: object + by_jenis: + additionalProperties: + type: integer + type: object + by_status: + additionalProperties: + type: integer + type: object + created_today: + type: integer + last_updated: + type: string + total_active: + type: integer + total_draft: + type: integer + total_inactive: + type: integer + updated_today: + type: integer + type: object + models.ErrorResponse: + properties: + code: + type: integer + error: + type: string + message: + type: string + timestamp: + type: string + type: object + models.ErrorResponseBpjs: + properties: + code: + type: string + errors: + additionalProperties: true + type: object + message: + type: string + request_id: + type: string + status: + type: string + type: object + models.LoginRequest: + properties: + password: + type: string + username: + type: string + required: + - password + - username + type: object + models.MetaResponse: + properties: + current_page: + type: integer + has_next: + type: boolean + has_prev: + type: boolean + limit: + type: integer + offset: + type: integer + total: + type: integer + total_pages: + type: integer + type: object + models.NullableInt32: + properties: + int32: + type: integer + valid: + type: boolean + type: object + models.NullableString: + properties: + string: + type: string + valid: + type: boolean + type: object + models.NullableTime: + properties: + time: + type: string + valid: + type: boolean + type: object + models.TokenResponse: + properties: + access_token: + type: string + expires_in: + type: integer + token_type: + type: string + type: object + models.User: + properties: + email: + type: string + id: + type: string + role: + type: string + username: + type: string + type: object + peserta.PesertaData: + properties: + cob: + properties: + nmAsuransi: {} + noAsuransi: {} + tglTAT: {} + tglTMT: {} + type: object + hakKelas: + properties: + keterangan: + type: string + kode: + type: string + type: object + informasi: + properties: + dinsos: {} + eSEP: {} + noSKTM: {} + prolanisPRB: + type: string + type: object + jenisPeserta: + properties: + keterangan: + type: string + kode: + type: string + type: object + mr: + properties: + noMR: + type: string + noTelepon: + type: string + type: object + nama: + type: string + nik: + type: string + noKartu: + type: string + pisa: + type: string + provUmum: + properties: + kdProvider: + type: string + nmProvider: + type: string + type: object + raw_response: + type: string + sex: + type: string + statusPeserta: + properties: + keterangan: + type: string + kode: + type: string + type: object + tglCetakKartu: + type: string + tglLahir: + type: string + tglTAT: + type: string + tglTMT: + type: string + umur: + properties: + umurSaatPelayanan: + type: string + umurSekarang: + type: string + type: object + type: object + peserta.PesertaResponse: + properties: + data: + $ref: '#/definitions/peserta.PesertaData' + message: + type: string + metaData: {} + request_id: + type: string + status: + type: string + timestamp: + type: string + type: object + retribusi.Retribusi: + properties: + date_created: + $ref: '#/definitions/models.NullableTime' + date_updated: + $ref: '#/definitions/models.NullableTime' + dinas: + $ref: '#/definitions/models.NullableString' + id: + type: string + jenis: + $ref: '#/definitions/models.NullableString' + kelompok_obyek: + $ref: '#/definitions/models.NullableString' + kode_tarif: + $ref: '#/definitions/models.NullableString' + pelayanan: + $ref: '#/definitions/models.NullableString' + rekening_denda: + $ref: '#/definitions/models.NullableString' + rekening_pokok: + $ref: '#/definitions/models.NullableString' + satuan: + $ref: '#/definitions/models.NullableString' + satuan_overtime: + $ref: '#/definitions/models.NullableString' + sort: + $ref: '#/definitions/models.NullableInt32' + status: + type: string + tarif: + $ref: '#/definitions/models.NullableString' + tarif_overtime: + $ref: '#/definitions/models.NullableString' + uraian_1: + $ref: '#/definitions/models.NullableString' + uraian_2: + $ref: '#/definitions/models.NullableString' + uraian_3: + $ref: '#/definitions/models.NullableString' + user_created: + $ref: '#/definitions/models.NullableString' + user_updated: + $ref: '#/definitions/models.NullableString' + type: object + retribusi.RetribusiCreateRequest: + properties: + dinas: + maxLength: 255 + minLength: 1 + type: string + jenis: + maxLength: 255 + minLength: 1 + type: string + kelompok_obyek: + maxLength: 255 + minLength: 1 + type: string + kode_tarif: + maxLength: 255 + minLength: 1 + type: string + pelayanan: + maxLength: 255 + minLength: 1 + type: string + rekening_denda: + maxLength: 255 + minLength: 1 + type: string + rekening_pokok: + maxLength: 255 + minLength: 1 + type: string + satuan: + maxLength: 255 + minLength: 1 + type: string + satuan_overtime: + maxLength: 255 + minLength: 1 + type: string + status: + enum: + - draft + - active + - inactive + type: string + tarif: + type: string + tarif_overtime: + type: string + uraian_1: + type: string + uraian_2: + type: string + uraian_3: + type: string + required: + - status + type: object + retribusi.RetribusiCreateResponse: + properties: + data: + $ref: '#/definitions/retribusi.Retribusi' + message: + type: string + type: object + retribusi.RetribusiDeleteResponse: + properties: + id: + type: string + message: + type: string + type: object + retribusi.RetribusiGetByIDResponse: + properties: + data: + $ref: '#/definitions/retribusi.Retribusi' + message: + type: string + type: object + retribusi.RetribusiGetResponse: + properties: + data: + items: + $ref: '#/definitions/retribusi.Retribusi' + type: array + message: + type: string + meta: + $ref: '#/definitions/models.MetaResponse' + summary: + $ref: '#/definitions/models.AggregateData' + type: object + retribusi.RetribusiUpdateRequest: + properties: + dinas: + maxLength: 255 + minLength: 1 + type: string + jenis: + maxLength: 255 + minLength: 1 + type: string + kelompok_obyek: + maxLength: 255 + minLength: 1 + type: string + kode_tarif: + maxLength: 255 + minLength: 1 + type: string + pelayanan: + maxLength: 255 + minLength: 1 + type: string + rekening_denda: + maxLength: 255 + minLength: 1 + type: string + rekening_pokok: + maxLength: 255 + minLength: 1 + type: string + satuan: + maxLength: 255 + minLength: 1 + type: string + satuan_overtime: + maxLength: 255 + minLength: 1 + type: string + status: + enum: + - draft + - active + - inactive + type: string + tarif: + type: string + tarif_overtime: + type: string + uraian_1: + type: string + uraian_2: + type: string + uraian_3: + type: string + required: + - status + type: object + retribusi.RetribusiUpdateResponse: + properties: + data: + $ref: '#/definitions/retribusi.Retribusi' + message: + type: string + type: object + rujukan.DataPeserta: + properties: + cob: + properties: + nmAsuransi: {} + noAsuransi: {} + tglTAT: {} + tglTMT: {} + type: object + hakKelas: + properties: + keterangan: + type: string + kode: + type: string + type: object + informasi: + properties: + dinsos: {} + noSKTM: {} + prolanisPRB: {} + type: object + jenisPeserta: + properties: + keterangan: + type: string + kode: + type: string + type: object + mr: + properties: + noMR: + type: string + noTelepon: {} + type: object + nama: + type: string + nik: + type: string + noKartu: + type: string + pisa: + type: string + provUmum: + properties: + kdProvider: + type: string + nmProvider: + type: string + type: object + sex: + type: string + statusPeserta: + properties: + keterangan: + type: string + kode: + type: string + type: object + tglCetakKartu: + type: string + tglLahir: + type: string + tglTAT: + type: string + tglTMT: + type: string + umur: + properties: + umurSaatPelayanan: + type: string + umurSekarang: + type: string + type: object + type: object + rujukan.DiagnosaData: + properties: + kode: + type: string + nama: + type: string + type: object + rujukan.PelayananData: + properties: + kode: + type: string + nama: + type: string + type: object + rujukan.PoliRujukanData: + properties: + kode: + type: string + nama: + type: string + type: object + rujukan.ProvPerujukData: + properties: + kode: + type: string + nama: + type: string + type: object + rujukan.RujukanData: + properties: + diagnosa: + $ref: '#/definitions/rujukan.DiagnosaData' + keluhan: + type: string + noKunjungan: + type: string + pelayanan: + $ref: '#/definitions/rujukan.PelayananData' + peserta: + $ref: '#/definitions/rujukan.DataPeserta' + poliRujukan: + $ref: '#/definitions/rujukan.PoliRujukanData' + provPerujuk: + $ref: '#/definitions/rujukan.ProvPerujukData' + tglKunjungan: + type: string + type: object + rujukan.RujukanRequest: + properties: + noKartu: + type: string + noRujukan: + type: string + request_id: + type: string + timestamp: + type: string + required: + - noRujukan + type: object + rujukan.RujukanResponse: + properties: + data: + $ref: '#/definitions/rujukan.RujukanData' + list: + items: + $ref: '#/definitions/rujukan.RujukanData' + type: array + message: + type: string + metaData: {} + request_id: + type: string + status: + type: string + timestamp: + type: string + type: object +host: localhost:8080 +info: + contact: + email: support@swagger.io + name: API Support + url: http://www.swagger.io/support + description: A comprehensive Go API service with Swagger documentation + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + termsOfService: http://swagger.io/terms/ + title: API Service + version: 1.0.0 +paths: + /Peserta/nik/:nik: + get: + consumes: + - application/json + description: Get participant eligibility information by NIK + parameters: + - description: Request ID for tracking + in: header + name: X-Request-ID + type: string + - description: nik + example: '"example_value"' + in: path + name: nik + required: true + type: string + produces: + - application/json + responses: + "200": + description: Successfully retrieved Bynik data + schema: + $ref: '#/definitions/peserta.PesertaResponse' + "400": + description: Bad request - invalid parameters + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "401": + description: Unauthorized - invalid API credentials + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "404": + description: Not found - Bynik not found + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + security: + - ApiKeyAuth: [] + summary: Get Bynik data + tags: + - Peserta + /Peserta/nokartu/:nokartu: + get: + consumes: + - application/json + description: Get participant eligibility information by card number + parameters: + - description: Request ID for tracking + in: header + name: X-Request-ID + type: string + - description: nokartu + example: '"example_value"' + in: path + name: nokartu + required: true + type: string + produces: + - application/json + responses: + "200": + description: Successfully retrieved Bynokartu data + schema: + $ref: '#/definitions/peserta.PesertaResponse' + "400": + description: Bad request - invalid parameters + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "401": + description: Unauthorized - invalid API credentials + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "404": + description: Not found - Bynokartu not found + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + security: + - ApiKeyAuth: [] + summary: Get Bynokartu data + tags: + - Peserta + /Rujukan/:norujukan: + delete: + consumes: + - application/json + description: Delete existing Rujukan from BPJS system + parameters: + - description: Request ID for tracking + in: header + name: X-Request-ID + type: string + produces: + - application/json + responses: + "200": + description: Successfully deleted Rujukan + schema: + $ref: '#/definitions/rujukan.RujukanResponse' + "400": + description: Bad request - invalid parameters + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "401": + description: Unauthorized - invalid API credentials + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "404": + description: Not found - Rujukan not found + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + security: + - ApiKeyAuth: [] + summary: Delete existing Rujukan + tags: + - Rujukan + post: + consumes: + - application/json + description: Create new Rujukan in BPJS system + parameters: + - description: Request ID for tracking + in: header + name: X-Request-ID + type: string + - description: Rujukan data + in: body + name: request + required: true + schema: + $ref: '#/definitions/rujukan.RujukanRequest' + produces: + - application/json + responses: + "201": + description: Successfully created Rujukan + schema: + $ref: '#/definitions/rujukan.RujukanResponse' + "400": + description: Bad request + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "409": + description: Conflict + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + security: + - ApiKeyAuth: [] + summary: Create new Rujukan + tags: + - Rujukan + put: + consumes: + - application/json + description: Update existing Rujukan in BPJS system + parameters: + - description: Request ID for tracking + in: header + name: X-Request-ID + type: string + - description: Rujukan update data + in: body + name: request + required: true + schema: + $ref: '#/definitions/rujukan.RujukanRequest' + produces: + - application/json + responses: + "200": + description: Successfully updated Rujukan + schema: + $ref: '#/definitions/rujukan.RujukanResponse' + "400": + description: Bad request - invalid parameters + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "401": + description: Unauthorized - invalid API credentials + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "404": + description: Not found - Rujukan not found + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "409": + description: Conflict - update conflict occurred + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + security: + - ApiKeyAuth: [] + summary: Update existing Rujukan + tags: + - Rujukan + /Rujukanbalik/:norujukan: + delete: + consumes: + - application/json + description: Delete existing Rujukanbalik from BPJS system + parameters: + - description: Request ID for tracking + in: header + name: X-Request-ID + type: string + produces: + - application/json + responses: + "200": + description: Successfully deleted Rujukanbalik + schema: + $ref: '#/definitions/rujukan.RujukanResponse' + "400": + description: Bad request - invalid parameters + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "401": + description: Unauthorized - invalid API credentials + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "404": + description: Not found - Rujukanbalik not found + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + security: + - ApiKeyAuth: [] + summary: Delete existing Rujukanbalik + tags: + - Rujukan + post: + consumes: + - application/json + description: Create new Rujukanbalik in BPJS system + parameters: + - description: Request ID for tracking + in: header + name: X-Request-ID + type: string + - description: Rujukanbalik data + in: body + name: request + required: true + schema: + $ref: '#/definitions/rujukan.RujukanRequest' + produces: + - application/json + responses: + "201": + description: Successfully created Rujukanbalik + schema: + $ref: '#/definitions/rujukan.RujukanResponse' + "400": + description: Bad request + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "409": + description: Conflict + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + security: + - ApiKeyAuth: [] + summary: Create new Rujukanbalik + tags: + - Rujukan + put: + consumes: + - application/json + description: Update existing Rujukanbalik in BPJS system + parameters: + - description: Request ID for tracking + in: header + name: X-Request-ID + type: string + - description: Rujukanbalik update data + in: body + name: request + required: true + schema: + $ref: '#/definitions/rujukan.RujukanRequest' + produces: + - application/json + responses: + "200": + description: Successfully updated Rujukanbalik + schema: + $ref: '#/definitions/rujukan.RujukanResponse' + "400": + description: Bad request - invalid parameters + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "401": + description: Unauthorized - invalid API credentials + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "404": + description: Not found - Rujukanbalik not found + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "409": + description: Conflict - update conflict occurred + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + security: + - ApiKeyAuth: [] + summary: Update existing Rujukanbalik + tags: + - Rujukan + /api/v1/auth/login: + post: + consumes: + - application/json + description: Authenticate user with username and password to receive JWT token + parameters: + - description: Login credentials + in: body + name: login + required: true + schema: + $ref: '#/definitions/models.LoginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.TokenResponse' + "400": + description: Bad request + schema: + additionalProperties: + type: string + type: object + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + summary: Login user and get JWT token + tags: + - Authentication + /api/v1/auth/me: + get: + description: Get information about the currently authenticated user + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.User' + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + security: + - Bearer: [] + summary: Get current user info + tags: + - Authentication + /api/v1/auth/refresh: + post: + consumes: + - application/json + description: Refresh the JWT token using a valid refresh token + parameters: + - description: Refresh token + in: body + name: refresh + required: true + schema: + additionalProperties: + type: string + type: object + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.TokenResponse' + "400": + description: Bad request + schema: + additionalProperties: + type: string + type: object + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + summary: Refresh JWT token + tags: + - Authentication + /api/v1/auth/register: + post: + consumes: + - application/json + description: Register a new user account + parameters: + - description: Registration data + in: body + name: register + required: true + schema: + additionalProperties: + type: string + type: object + produces: + - application/json + responses: + "201": + description: Created + schema: + additionalProperties: + type: string + type: object + "400": + description: Bad request + schema: + additionalProperties: + type: string + type: object + summary: Register new user + tags: + - Authentication + /api/v1/retribusi/{id}: + delete: + consumes: + - application/json + description: Soft deletes a retribusi by setting status to 'deleted' + parameters: + - description: Retribusi ID (UUID) + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Retribusi deleted successfully + schema: + $ref: '#/definitions/retribusi.RetribusiDeleteResponse' + "400": + description: Invalid ID format + schema: + $ref: '#/definitions/models.ErrorResponse' + "404": + description: Retribusi not found + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.ErrorResponse' + summary: Delete retribusi + tags: + - Retribusi + get: + consumes: + - application/json + description: Returns a single retribusi by ID + parameters: + - description: Retribusi ID (UUID) + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success response + schema: + $ref: '#/definitions/retribusi.RetribusiGetByIDResponse' + "400": + description: Invalid ID format + schema: + $ref: '#/definitions/models.ErrorResponse' + "404": + description: Retribusi not found + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.ErrorResponse' + summary: Get Retribusi by ID + tags: + - Retribusi + put: + consumes: + - application/json + description: Updates an existing retribusi record + parameters: + - description: Retribusi ID (UUID) + in: path + name: id + required: true + type: string + - description: Retribusi update request + in: body + name: request + required: true + schema: + $ref: '#/definitions/retribusi.RetribusiUpdateRequest' + produces: + - application/json + responses: + "200": + description: Retribusi updated successfully + schema: + $ref: '#/definitions/retribusi.RetribusiUpdateResponse' + "400": + description: Bad request or validation error + schema: + $ref: '#/definitions/models.ErrorResponse' + "404": + description: Retribusi not found + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.ErrorResponse' + summary: Update retribusi + tags: + - Retribusi + /api/v1/retribusis: + get: + consumes: + - application/json + description: Returns a paginated list of retribusis with optional summary statistics + parameters: + - default: 10 + description: Limit (max 100) + in: query + name: limit + type: integer + - default: 0 + description: Offset + in: query + name: offset + type: integer + - default: false + description: Include aggregation summary + in: query + name: include_summary + type: boolean + - description: Filter by status + in: query + name: status + type: string + - description: Filter by jenis + in: query + name: jenis + type: string + - description: Filter by dinas + in: query + name: dinas + type: string + - description: Search in multiple fields + in: query + name: search + type: string + produces: + - application/json + responses: + "200": + description: Success response + schema: + $ref: '#/definitions/retribusi.RetribusiGetResponse' + "400": + description: Bad request + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.ErrorResponse' + summary: Get retribusi with pagination and optional aggregation + tags: + - Retribusi + post: + consumes: + - application/json + description: Creates a new retribusi record + parameters: + - description: Retribusi creation request + in: body + name: request + required: true + schema: + $ref: '#/definitions/retribusi.RetribusiCreateRequest' + produces: + - application/json + responses: + "201": + description: Retribusi created successfully + schema: + $ref: '#/definitions/retribusi.RetribusiCreateResponse' + "400": + description: Bad request or validation error + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.ErrorResponse' + summary: Create retribusi + tags: + - Retribusi + /api/v1/retribusis/dynamic: + get: + consumes: + - application/json + description: Returns retribusis with advanced dynamic filtering like Directus + parameters: + - description: Fields to select (e.g., fields=*.*) + in: query + name: fields + type: string + - description: Dynamic filters (e.g., filter[Jenis][_eq]=value) + in: query + name: filter[column][operator] + type: string + - description: Sort fields (e.g., sort=date_created,-Jenis) + in: query + name: sort + type: string + - default: 10 + description: Limit + in: query + name: limit + type: integer + - default: 0 + description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: Success response + schema: + $ref: '#/definitions/retribusi.RetribusiGetResponse' + "400": + description: Bad request + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.ErrorResponse' + summary: Get retribusi with dynamic filtering + tags: + - Retribusi + /api/v1/retribusis/stats: + get: + consumes: + - application/json + description: Returns comprehensive statistics about retribusi data + parameters: + - description: Filter statistics by status + in: query + name: status + type: string + produces: + - application/json + responses: + "200": + description: Statistics data + schema: + $ref: '#/definitions/models.AggregateData' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.ErrorResponse' + summary: Get retribusi statistics + tags: + - Retribusi + /api/v1/token/generate: + post: + consumes: + - application/json + description: Generate a JWT token for a user + parameters: + - description: User credentials + in: body + name: token + required: true + schema: + $ref: '#/definitions/models.LoginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.TokenResponse' + "400": + description: Bad request + schema: + additionalProperties: + type: string + type: object + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + summary: Generate JWT token + tags: + - Token + /api/v1/token/generate-direct: + post: + consumes: + - application/json + description: Generate a JWT token directly without password verification (for + testing) + parameters: + - description: User info + in: body + name: user + required: true + schema: + additionalProperties: + type: string + type: object + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.TokenResponse' + "400": + description: Bad request + schema: + additionalProperties: + type: string + type: object + summary: Generate token directly + tags: + - Token + /bynokartu/:nokartu: + get: + consumes: + - application/json + description: Get rujukan by card number + parameters: + - description: Request ID for tracking + in: header + name: X-Request-ID + type: string + - description: nokartu + example: '"example_value"' + in: path + name: nokartu + required: true + type: string + produces: + - application/json + responses: + "200": + description: Successfully retrieved Bynokartu data + schema: + $ref: '#/definitions/rujukan.RujukanResponse' + "400": + description: Bad request - invalid parameters + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "401": + description: Unauthorized - invalid API credentials + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "404": + description: Not found - Bynokartu not found + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + security: + - ApiKeyAuth: [] + summary: Get Bynokartu data + tags: + - Rujukan + /bynorujukan/:norujukan: + get: + consumes: + - application/json + description: Get rujukan by nomor rujukan + parameters: + - description: Request ID for tracking + in: header + name: X-Request-ID + type: string + - description: norujukan + example: '"example_value"' + in: path + name: norujukan + required: true + type: string + produces: + - application/json + responses: + "200": + description: Successfully retrieved Bynorujukan data + schema: + $ref: '#/definitions/rujukan.RujukanResponse' + "400": + description: Bad request - invalid parameters + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "401": + description: Unauthorized - invalid API credentials + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "404": + description: Not found - Bynorujukan not found + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.ErrorResponseBpjs' + security: + - ApiKeyAuth: [] + summary: Get Bynorujukan data + tags: + - Rujukan +schemes: +- http +- https +swagger: "2.0" diff --git a/example.env b/example.env new file mode 100644 index 0000000..6c3222f --- /dev/null +++ b/example.env @@ -0,0 +1,92 @@ +# Server Configuration +PORT=8080 +GIN_MODE=debug + +# Default Database Configuration (PostgreSQL) +DB_CONNECTION=postgres +DB_USERNAME=stim +DB_PASSWORD=stim*RS54 +DB_HOST=10.10.123.165 +DB_DATABASE=satu_db +DB_PORT=5000 +DB_SSLMODE=disable + +# satudata Database Configuration (PostgreSQL) +# POSTGRES_CONNECTION=postgres +# POSTGRES_USERNAME=stim +# POSTGRES_PASSWORD=stim*RS54 +# POSTGRES_HOST=10.10.123.165 +# POSTGRES_DATABASE=satu_db +# POSTGRES_NAME=satu_db +# POSTGRES_PORT=5000 +# POSTGRES_SSLMODE=disable + + +POSTGRES_SATUDATA_CONNECTION=postgres +POSTGRES_SATUDATA_USERNAME=stim +POSTGRES_SATUDATA_PASSWORD=stim*RS54 +POSTGRES_SATUDATA_HOST=10.10.123.165 +POSTGRES_SATUDATA_DATABASE=satu_db +POSTGRES_SATUDATA_PORT=5000 +POSTGRES_SATUDATA_SSLMODE=disable + +# Mongo Database +MONGODB_MONGOHL7_CONNECTION=mongodb +MONGODB_MONGOHL7_HOST=10.10.123.206 +MONGODB_MONGOHL7_PORT=27017 +MONGODB_MONGOHL7_USER=admin +MONGODB_MONGOHL7_PASS=stim*rs54 +MONGODB_MONGOHL7_MASTER=master +MONGODB_MONGOHL7_LOCAL=local +MONGODB_MONGOHL7_SSLMODE=disable + +# MYSQL Antrian Database +MYSQL_ANTRIAN_CONNECTION=mysql +MYSQL_ANTRIAN_HOST=10.10.123.163 +MYSQL_ANTRIAN_USERNAME=www-data +MYSQL_ANTRIAN_PASSWORD=www-data +MYSQL_ANTRIAN_DATABASE=antrian_rssa +MYSQL_ANTRIAN_PORT=3306 +MYSQL_ANTRIAN_SSLMODE=disable + + +MYSQL_MEDICAL_CONNECTION=mysql +MYSQL_MEDICAL_HOST=10.10.123.147 +MYSQL_MEDICAL_USERNAME=meninjardev +MYSQL_MEDICAL_PASSWORD=meninjar*RS54 +MYSQL_MEDICAL_DATABASE=healtcare_database +MYSQL_MEDICAL_PORT=3306 +MYSQL_MEDICAL_SSLMODE=disable + +# Keycloak Configuration (optional) +KEYCLOAK_ISSUER=https://auth.rssa.top/realms/sandbox +KEYCLOAK_AUDIENCE=nuxtsim-pendaftaran +KEYCLOAK_JWKS_URL=https://auth.rssa.top/realms/sandbox/protocol/openid-connect/certs +KEYCLOAK_ENABLED=true + +# BPJS Configuration +BPJS_BASEURL=https://apijkn.bpjs-kesehatan.go.id/vclaim-rest +BPJS_CONSID=5257 +BPJS_USERKEY=4cf1cbef8c008440bbe9ef9ba789e482 +BPJS_SECRETKEY=1bV363512D + +BRIDGING_SATUSEHAT_ORG_ID=100026555 +BRIDGING_SATUSEHAT_FASYAKES_ID=3573011 +BRIDGING_SATUSEHAT_CLIENT_ID=l1ZgJGW6K5pnrqGUikWM7fgIoquA2AQ5UUG0U8WqHaq2VEyZ +BRIDGING_SATUSEHAT_CLIENT_SECRET=Al3PTYAW6axPiAFwaFlpn8qShLFW5YGMgG8w1qhexgCc7lGTEjjcR6zxa06ThPDy +BRIDGING_SATUSEHAT_AUTH_URL=https://api-satusehat.kemkes.go.id/oauth2/v1 +BRIDGING_SATUSEHAT_BASE_URL=https://api-satusehat.kemkes.go.id/fhir-r4/v1 +BRIDGING_SATUSEHAT_CONSENT_URL=https://api-satusehat.dto.kemkes.go.id/consent/v1 +BRIDGING_SATUSEHAT_KFA_URL=https://api-satusehat.kemkes.go.id/kfa-v2 + +SWAGGER_TITLE=My Custom API Service +SWAGGER_DESCRIPTION=This is a custom API service for managing various resources +SWAGGER_VERSION=2.0.0 +SWAGGER_CONTACT_NAME=STIM IT Support +SWAGGER_HOST=api.mycompany.com:8080 +SWAGGER_BASE_PATH=/api/v2 +SWAGGER_SCHEMES=https + +API_TITLE=API Service UJICOBA +API_DESCRIPTION=Dokumentation SWAGGER +API_VERSION=3.0.0 diff --git a/examples/clientsocket/.gitignore b/examples/clientsocket/.gitignore new file mode 100644 index 0000000..4a7f73a --- /dev/null +++ b/examples/clientsocket/.gitignore @@ -0,0 +1,24 @@ +# Nuxt dev/build outputs +.output +.data +.nuxt +.nitro +.cache +dist + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Misc +.DS_Store +.fleet +.idea + +# Local env files +.env +.env.* +!.env.example diff --git a/examples/clientsocket/README.md b/examples/clientsocket/README.md new file mode 100644 index 0000000..25b5821 --- /dev/null +++ b/examples/clientsocket/README.md @@ -0,0 +1,75 @@ +# Nuxt Minimal Starter + +Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. + +## Setup + +Make sure to install dependencies: + +```bash +# npm +npm install + +# pnpm +pnpm install + +# yarn +yarn install + +# bun +bun install +``` + +## Development Server + +Start the development server on `http://localhost:3000`: + +```bash +# npm +npm run dev + +# pnpm +pnpm dev + +# yarn +yarn dev + +# bun +bun run dev +``` + +## Production + +Build the application for production: + +```bash +# npm +npm run build + +# pnpm +pnpm build + +# yarn +yarn build + +# bun +bun run build +``` + +Locally preview production build: + +```bash +# npm +npm run preview + +# pnpm +pnpm preview + +# yarn +yarn preview + +# bun +bun run preview +``` + +Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. diff --git a/examples/clientsocket/app/app.vue b/examples/clientsocket/app/app.vue new file mode 100644 index 0000000..09f935b --- /dev/null +++ b/examples/clientsocket/app/app.vue @@ -0,0 +1,6 @@ + diff --git a/examples/clientsocket/assets/css/main.css b/examples/clientsocket/assets/css/main.css new file mode 100644 index 0000000..e3c5807 --- /dev/null +++ b/examples/clientsocket/assets/css/main.css @@ -0,0 +1,672 @@ +/* Enhanced WebSocket Client Styles */ +* { + box-sizing: border-box; +} + +body { + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + margin: 0; + padding: 20px; + background-color: #f5f5f5; + color: #333; +} + +.container { + max-width: 1400px; + margin: 0 auto; + background: white; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + padding: 24px; + overflow: hidden; +} + +h1 { + color: #333; + text-align: center; + margin-bottom: 32px; + font-weight: 600; +} + +.status-bar { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; + padding: 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 12px; + margin-bottom: 24px; +} + +.status-item { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} + +.status-indicator { + width: 12px; + height: 12px; + border-radius: 50%; + display: inline-block; +} + +.status-connected { + background: #4CAF50; + box-shadow: 0 0 8px rgba(76, 175, 80, 0.6); +} + +.status-connecting { + background: #FFC107; + animation: pulse 1.5s infinite; +} + +.status-disconnected { + background: #F44336; +} + +@keyframes pulse { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +} + +.health-indicator { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: 6px; + font-size: 12px; + font-weight: bold; +} + +.health-excellent { + background: #d4edda; + color: #155724; +} + +.health-good { + background: #d1ecf1; + color: #0c5460; +} + +.health-warning { + background: #fff3cd; + color: #856404; +} + +.health-poor { + background: #f8d7da; + color: #721c24; +} + +.tabs { + display: flex; + border-bottom: 2px solid #e9ecef; + margin-bottom: 24px; + overflow-x: auto; + background: white; + border-radius: 8px 8px 0 0; +} + +.tab { + padding: 16px 24px; + background: transparent; + border: none; + border-bottom: 3px solid transparent; + cursor: pointer; + white-space: nowrap; + transition: all 0.3s ease; + font-weight: 500; + color: #666; +} + +.tab:hover { + background: #f8f9fa; + color: #333; +} + +.tab.active { + border-bottom-color: #1976d2; + color: #1976d2; + font-weight: 600; + background: #f8f9fa; +} + +.tab-content { + display: none; + animation: fadeIn 0.3s ease; +} + +.tab-content.active { + display: block; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.controls { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; + margin-bottom: 24px; + padding: 24px; + background: #f8f9fa; + border-radius: 8px; +} + +.input-group { + display: flex; + flex-direction: column; + gap: 12px; +} + +.input-group label { + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.input-group input, +.input-group select, +.input-group textarea { + padding: 12px 16px; + border: 2px solid #e9ecef; + border-radius: 6px; + font-size: 14px; + transition: border-color 0.3s ease; +} + +.input-group input:focus, +.input-group select:focus, +.input-group textarea:focus { + outline: none; + border-color: #1976d2; + box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1); +} + +.checkbox-group { + display: flex; + align-items: center; + gap: 8px; + margin: 8px 0; +} + +.checkbox-group input[type="checkbox"] { + margin: 0; + width: 18px; + height: 18px; +} + +.btn { + padding: 12px 24px; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + font-weight: 600; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.btn-primary { + background: #1976d2; + color: white; +} + +.btn-primary:hover { + background: #1565c0; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3); +} + +.btn-secondary { + background: #6c757d; + color: white; +} + +.btn-secondary:hover { + background: #5a6268; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3); +} + +.btn-warning { + background: #FFC107; + color: #212529; +} + +.btn-warning:hover { + background: #e0a800; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(255, 193, 7, 0.3); +} + +.btn-info { + background: #17a2b8; + color: white; +} + +.btn-info:hover { + background: #138496; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(23, 162, 184, 0.3); +} + +.btn-danger { + background: #dc3545; + color: white; +} + +.btn-danger:hover { + background: #c82333; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3); +} + +.btn-success { + background: #28a745; + color: white; +} + +.btn-success:hover { + background: #218838; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3); +} + +.messages-container { + height: 500px; + overflow-y: auto; + border: 2px solid #e9ecef; + border-radius: 8px; + padding: 20px; + background: #fafafa; + font-family: "SF Mono", "Monaco", "Inconsolata", "Roboto Mono", monospace; + font-size: 13px; + line-height: 1.5; +} + +.messages-container::-webkit-scrollbar { + width: 8px; +} + +.messages-container::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +.messages-container::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} + +.messages-container::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + +.message { + margin-bottom: 16px; + padding: 16px; + border-radius: 8px; + border-left: 4px solid #1976d2; + background: white; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + animation: slideIn 0.3s ease; +} + +@keyframes slideIn { + from { opacity: 0; transform: translateX(-20px); } + to { opacity: 1; transform: translateX(0); } +} + +.message.error { + border-left-color: #dc3545; + background: #fff5f5; +} + +.message.warning { + border-left-color: #FFC107; + background: #fffbf0; +} + +.message.info { + border-left-color: #17a2b8; + background: #f0f9ff; +} + +.message.success { + border-left-color: #28a745; + background: #f0fff4; +} + +.message-time { + font-size: 11px; + color: #666; + margin-bottom: 8px; + font-weight: 500; +} + +.message-type { + font-weight: 600; + color: #333; + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin: 24px 0; +} + +.stat-card { + background: white; + padding: 24px; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + text-align: center; + border: 1px solid #e9ecef; +} + +.stat-number { + font-size: 2.5em; + font-weight: 700; + color: #1976d2; + margin-bottom: 8px; +} + +.stat-label { + color: #666; + font-size: 14px; + font-weight: 500; +} + +.online-users { + max-height: 400px; + overflow-y: auto; + background: #f8f9fa; + border-radius: 8px; + padding: 20px; + border: 1px solid #e9ecef; +} + +.online-users::-webkit-scrollbar { + width: 6px; +} + +.online-users::-webkit-scrollbar-track { + background: #f1f1f1; +} + +.online-users::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 3px; +} + +.user-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + margin: 8px 0; + background: white; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + border: 1px solid #e9ecef; +} + +.user-info { + display: flex; + flex-direction: column; + gap: 4px; +} + +.user-id { + font-weight: 600; + color: #333; +} + +.user-details { + font-size: 12px; + color: #666; +} + +.loading-indicator { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid #f3f3f3; + border-top: 2px solid #1976d2; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-left: 8px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.message-limit-warning { + background: #fff3cd; + border: 1px solid #ffeaa7; + padding: 16px; + margin: 16px 0; + border-radius: 6px; + font-size: 14px; + color: #856404; + display: flex; + align-items: center; + gap: 8px; +} + +.admin-controls { + background: #fff3cd; + padding: 20px; + border-radius: 8px; + margin-bottom: 24px; + border: 1px solid #ffeaa7; +} + +.admin-warning { + color: #856404; + font-weight: 600; + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 8px; +} + +pre { + background: #f8f9fa; + padding: 12px; + border-radius: 6px; + overflow-x: auto; + font-size: 12px; + border: 1px solid #e9ecef; + margin: 8px 0; +} + +code { + background: #f8f9fa; + padding: 2px 6px; + border-radius: 3px; + font-family: "SF Mono", "Monaco", "Inconsolata", "Roboto Mono", monospace; + font-size: 13px; +} + +.message pre { + white-space: pre-wrap; + word-break: break-word; +} + +.message code { + display: block; + margin: 8px 0; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .container { + margin: 10px; + padding: 16px; + border-radius: 8px; + } + + .controls { + grid-template-columns: 1fr; + padding: 16px; + } + + .status-bar { + grid-template-columns: 1fr; + padding: 16px; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .tabs { + flex-wrap: wrap; + } + + .tab { + padding: 12px 16px; + font-size: 14px; + } + + .messages-container { + height: 300px; + font-size: 12px; + } + + .message { + padding: 12px; + font-size: 13px; + } + + .btn { + padding: 10px 16px; + font-size: 13px; + } +} + +@media (max-width: 480px) { + .container { + padding: 12px; + } + + .controls { + padding: 12px; + } + + .status-bar { + padding: 12px; + } + + .messages-container { + height: 250px; + padding: 12px; + } + + .tab { + padding: 8px 12px; + font-size: 12px; + } +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + body { + background-color: #121212; + color: #e0e0e0; + } + + .container { + background: #1e1e1e; + border: 1px solid #333; + } + + .controls { + background: #2a2a2a; + border: 1px solid #404040; + } + + .message { + background: #2a2a2a; + border: 1px solid #404040; + } + + .stat-card { + background: #2a2a2a; + border: 1px solid #404040; + } + + .online-users { + background: #2a2a2a; + border: 1px solid #404040; + } + + .user-item { + background: #333; + border: 1px solid #404040; + } + + .messages-container { + background: #1a1a1a; + border-color: #404040; + } + + pre { + background: #2a2a2a; + border-color: #404040; + } + + code { + background: #333; + } +} + +/* Accessibility improvements */ +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* Focus styles for keyboard navigation */ +.btn:focus, +.tab:focus, +.input-group input:focus, +.input-group select:focus, +.input-group textarea:focus { + outline: 2px solid #1976d2; + outline-offset: 2px; +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + .btn { + border: 2px solid currentColor; + } + + .message { + border: 2px solid currentColor; + } + + .stat-card { + border: 2px solid currentColor; + } +} diff --git a/examples/clientsocket/client.html b/examples/clientsocket/client.html new file mode 100644 index 0000000..e2dd860 --- /dev/null +++ b/examples/clientsocket/client.html @@ -0,0 +1,2251 @@ + + + + WebSocket Test Client + + + +
+

๐ŸŒ WebSocket Test Client - Enhanced & Optimized

+ + +
+
+ + Status: + Disconnected + +
+
+ Health: +
+ ๐Ÿ”ด Poor +
+
+
+ Uptime: 0s +
+
+ Client ID: + Not connected +
+
+ Reconnect: Not needed +
+
+ + + + + +
+ + + + + +
+ + +
+
+
+ + + + +
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+ + + + + +
+
+
+ + +
+
+
+ + + +
+ +
+ + + + +
+ +
+ + + + + +
+
+
+ + +
+
+
+ + + + + +
+ +
+ + + + +
+
+
+ + +
+
+
+
0
+
Messages Received
+
+
+
0
+
Messages Sent
+
+
+
0ms
+
Connection Latency
+
+
+
0
+
Reconnection Attempts
+
+
+ +
+
+ + +
+
+ No users data available. Click "Refresh Users" to load. +
+
+
+
+
+ + +
+
+
+ โš ๏ธ Admin functions - Use with caution! +
+
+
+ + + + +
+ +
+ + + + +
+
+
+
+ + +
+

+ ๐Ÿ“จ Messages + 0 messages + + +

+
+
+
+ + + + diff --git a/examples/clientsocket/components/WebSocketClient.vue b/examples/clientsocket/components/WebSocketClient.vue new file mode 100644 index 0000000..9e35f9d --- /dev/null +++ b/examples/clientsocket/components/WebSocketClient.vue @@ -0,0 +1,367 @@ + + + + + diff --git a/examples/clientsocket/components/tabs/AdminTab.vue b/examples/clientsocket/components/tabs/AdminTab.vue new file mode 100644 index 0000000..eacad47 --- /dev/null +++ b/examples/clientsocket/components/tabs/AdminTab.vue @@ -0,0 +1,665 @@ + + + + + diff --git a/examples/clientsocket/components/tabs/ConnectionTab.vue b/examples/clientsocket/components/tabs/ConnectionTab.vue new file mode 100644 index 0000000..988e02f --- /dev/null +++ b/examples/clientsocket/components/tabs/ConnectionTab.vue @@ -0,0 +1,397 @@ + + + + + diff --git a/examples/clientsocket/components/tabs/DatabaseTab.vue b/examples/clientsocket/components/tabs/DatabaseTab.vue new file mode 100644 index 0000000..6879f18 --- /dev/null +++ b/examples/clientsocket/components/tabs/DatabaseTab.vue @@ -0,0 +1,536 @@ + + + + + diff --git a/examples/clientsocket/components/tabs/MessagingTab.vue b/examples/clientsocket/components/tabs/MessagingTab.vue new file mode 100644 index 0000000..5d4dd99 --- /dev/null +++ b/examples/clientsocket/components/tabs/MessagingTab.vue @@ -0,0 +1,602 @@ + + + + + diff --git a/examples/clientsocket/components/tabs/MonitoringTab.vue b/examples/clientsocket/components/tabs/MonitoringTab.vue new file mode 100644 index 0000000..ad12093 --- /dev/null +++ b/examples/clientsocket/components/tabs/MonitoringTab.vue @@ -0,0 +1,637 @@ + + + + + diff --git a/examples/clientsocket/composables/note b/examples/clientsocket/composables/note new file mode 100644 index 0000000..7478c16 --- /dev/null +++ b/examples/clientsocket/composables/note @@ -0,0 +1,1073 @@ +import { ref, computed, reactive, nextTick } from "vue"; +import type { + WebSocketMessage, + ConnectionState, + WebSocketConfig, + MessageHistory, + ConnectionStats, + MonitoringData, + ClientInfo, + OnlineUser, + ActivityLog, +} from "../types/websocket"; + +export const useWebSocket = () => { + // Check if we're in browser environment + const isBrowser = process.client; + + const ws = ref(null); + const isConnected = ref(false); + const isConnecting = ref(false); + const connectionStatus = ref< + "disconnected" | "connecting" | "connected" | "error" + >("disconnected"); + + const connectionState = reactive({ + isConnected: false, + isConnecting: false, + connectionStatus: "disconnected", + clientId: null, + staticId: null, + currentRoom: null, + userId: "anonymous", + ipAddress: null, + connectionStartTime: null, + lastPingTime: null, + connectionLatency: 0, + connectionHealth: "poor", + reconnectAttempts: 0, + messagesReceived: 0, + messagesSent: 0, + uptime: "00:00:00", + }); + + const config = reactive({ + wsUrl: "ws://localhost:8080/api/v1/ws", + userId: "anonymous", + room: "default", + staticId: "", + useIPBasedId: false, + autoReconnect: true, + heartbeatEnabled: true, + maxReconnectAttempts: 10, + reconnectDelay: 1000, + maxReconnectDelay: 30000, + heartbeatInterval: 30000, + heartbeatTimeout: 5000, + maxMissedHeartbeats: 3, + maxMessages: 1000, + messageWarningThreshold: 800, + actionThrottle: 100, + }); + + const messages = ref([]); + const stats = ref(null); + const monitoringData = ref(null); + const onlineUsers = ref([]); + const activityLog = ref([]); + + let reconnectTimeout: number | null = null; + let heartbeatInterval: number | null = null; + let heartbeatTimeout: number | null = null; + let missedHeartbeats = 0; + let lastHeartbeatTime = 0; + let messageCount = 0; + let connectionStartTime: number | null = null; + let uptimeInterval: number | null = null; + let isManualDisconnect = false; + let reconnectAttempts = 0; + let connectionHealth: "excellent" | "good" | "warning" | "poor" = "poor"; + + const updateConnectionStatus = ( + status: "disconnected" | "connecting" | "connected" | "error" + ) => { + connectionStatus.value = status; + connectionState.connectionStatus = status; + + switch (status) { + case "connected": + isConnected.value = true; + isConnecting.value = false; + connectionState.isConnected = true; + connectionState.isConnecting = false; + connectionState.connectionHealth = "good"; + break; + case "connecting": + isConnected.value = false; + isConnecting.value = true; + connectionState.isConnected = false; + connectionState.isConnecting = true; + connectionState.connectionHealth = "warning"; + break; + case "disconnected": + isConnected.value = false; + isConnecting.value = false; + connectionState.isConnected = false; + connectionState.isConnecting = false; + connectionState.connectionHealth = "poor"; + break; + case "error": + isConnected.value = false; + isConnecting.value = false; + connectionState.isConnected = false; + connectionState.isConnecting = false; + connectionState.connectionHealth = "poor"; + break; + } + }; + + const updateUptime = () => { + if (connectionStartTime && isConnected.value) { + const now = Date.now(); + const uptime = now - connectionStartTime; + const seconds = Math.floor(uptime / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + connectionState.uptime = `${hours.toString().padStart(2, "0")}:${( + minutes % 60 + ) + .toString() + .padStart(2, "0")}:${(seconds % 60).toString().padStart(2, "0")}`; + } else { + connectionState.uptime = "00:00:00"; + } + }; + + const startUptimeTimer = () => { + if (uptimeInterval) { + clearInterval(uptimeInterval); + } + uptimeInterval = window.setInterval(updateUptime, 1000); + }; + + const stopUptimeTimer = () => { + if (uptimeInterval) { + clearInterval(uptimeInterval); + uptimeInterval = null; + } + }; + + // Only run WebSocket logic in browser + if (isBrowser) { + // Initialize connection state + updateConnectionStatus("disconnected"); + } + + const addMessage = ( + type: string, + data: any, + messageId?: string, + icon?: string + ) => { + if (!isBrowser) return; + + const now = new Date(); + const timeString = now.toLocaleTimeString("en-US", { + hour12: true, + hour: "numeric", + minute: "2-digit", + second: "2-digit", + }); + + // Enhanced message formatting based on type + let formattedData = data; + let displayType = type; + + switch (type) { + case "connection_info": + displayType = "info"; + formattedData = { + client_id: data.clientId, + connected_at: data.connectedAt, + ip_address: data.ipAddress, + last_ping: data.lastPingTime || "N/A", + room: data.room || "default", + static_id: data.staticId || "none", + user_id: data.userId || "anonymous", + }; + break; + + case "heartbeat": + displayType = "info"; + formattedData = `โค๏ธ Heartbeat started\nInterval: ${ + config.heartbeatInterval / 1000 + }s`; + break; + + case "pong": + displayType = "success"; + formattedData = `๐Ÿ“ Pong received\nLatency: ${connectionState.connectionLatency}ms`; + break; + + case "message": + displayType = "info"; + formattedData = `๐Ÿ“จ Message received\n${JSON.stringify(data, null, 2)}`; + break; + + case "broadcast": + displayType = "info"; + formattedData = `๐Ÿ“ก Broadcast received\n${data}`; + break; + + case "direct_message": + displayType = "info"; + formattedData = `๐Ÿ“จ Direct message from ${data.from}\n${data.message}`; + break; + + case "room_message": + displayType = "info"; + formattedData = `๐Ÿ“ข Room message from ${data.room}\n${data.message}`; + break; + + case "stats": + displayType = "info"; + formattedData = `๐Ÿ“Š Stats updated\n${JSON.stringify(data, null, 2)}`; + break; + + case "monitoring": + displayType = "info"; + formattedData = `๐Ÿ“ˆ Monitoring data\n${JSON.stringify(data, null, 2)}`; + break; + + case "online_users": + displayType = "info"; + formattedData = `๐Ÿ‘ฅ Online users updated\nCount: ${ + data.users?.length || 0 + }`; + break; + + case "server_info": + displayType = "info"; + formattedData = `๐Ÿ”ง Server info\n${JSON.stringify(data, null, 2)}`; + break; + + case "system_health": + displayType = "info"; + formattedData = `๐Ÿ’š System health\n${JSON.stringify(data, null, 2)}`; + break; + + default: + if (typeof data === "string") { + formattedData = data; + } else { + formattedData = JSON.stringify(data, null, 2); + } + } + + const message: MessageHistory = { + timestamp: now, + type: displayType, + data: formattedData, + messageId, + size: JSON.stringify(data).length, + icon: icon || getIconForMessageType(displayType), + timeString: timeString, + }; + + messages.value.unshift(message); + messageCount++; + + // Keep only the last maxMessages + if (messages.value.length > config.maxMessages) { + messages.value = messages.value.slice(0, config.maxMessages); + } + + // Update connection state + connectionState.messagesReceived++; + }; + + const getIconForMessageType = (type: string): string => { + switch (type) { + case "success": + return "๐ŸŸข"; + case "error": + return "โŒ"; + case "warning": + return "โš ๏ธ"; + case "info": + return "โ„น๏ธ"; + default: + return "๐Ÿ“"; + } + }; + + const connectionHealthColor = computed(() => { + switch (connectionState.connectionHealth) { + case "excellent": + return "#4CAF50"; + case "good": + return "#2196F3"; + case "warning": + return "#FFC107"; + case "poor": + return "#F44336"; + default: + return "#9E9E9E"; + } + }); + + const connectionHealthText = computed(() => { + switch (connectionState.connectionHealth) { + case "excellent": + return "Excellent"; + case "good": + return "Good"; + case "warning": + return "Warning"; + case "poor": + return "Poor"; + default: + return "Unknown"; + } + }); + + // Admin functionality + const serverInfo = ref(null); + const systemHealth = ref(null); + + const executeAdminCommand = async (command: string, params: any) => { + if (!isBrowser || !ws.value) throw new Error("Not connected"); + + const message = { + type: "admin_command", + command, + params, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + return { success: true, message: "Command sent successfully" }; + }; + + const getServerInfo = async () => { + if (!isBrowser || !ws.value) throw new Error("Not connected"); + + const message = { + type: "get_server_info", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + }; + + const getSystemHealth = async () => { + if (!isBrowser || !ws.value) throw new Error("Not connected"); + + const message = { + type: "get_system_health", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + }; + + // Cleanup on unmount + const cleanup = () => { + if (!isBrowser) return; + + disconnect(); + if (reconnectTimeout) { + clearTimeout(reconnectTimeout); + } + stopHeartbeat(); + }; + + // WebSocket connection methods (only available in browser) + const connect = () => { + if (!isBrowser) return; + + // Prevent multiple connection attempts + if (isConnecting.value || isConnected.value) { + return; + } + + // Clean up existing connection + if (ws.value) { + cleanupConnection(); + } + + updateConnectionStatus("connecting"); + isManualDisconnect = false; + connectionStartTime = Date.now(); + startUptimeTimer(); + + try { + // Build WebSocket URL with parameters + let url = config.wsUrl; + const params = new URLSearchParams(); + + if (config.userId) { + params.append("user_id", config.userId); + } + if (config.room) { + params.append("room", config.room); + } + if (config.useIPBasedId) { + params.append("ip_based", "true"); + } else if (config.staticId) { + params.append("static_id", config.staticId); + } + + if (params.toString()) { + url += "?" + params.toString(); + } + + ws.value = new WebSocket(url); + + // Connection timeout + const connectionTimeout = setTimeout(() => { + if (ws.value && ws.value.readyState === WebSocket.CONNECTING) { + ws.value.close(); + updateConnectionStatus("error"); + addMessage("โŒ Connection timeout after 15 seconds", "error"); + scheduleReconnect(); + } + }, 15000); + + ws.value.onopen = function (event) { + clearTimeout(connectionTimeout); + updateConnectionStatus("connected"); + reconnectAttempts = 0; + connectionHealth = "good"; + connectionState.connectionStartTime = connectionStartTime; + + addMessage( + "๐ŸŸข Connected to WebSocket server", + "success", + "Connection established successfully" + ); + + // Start heartbeat if enabled + if (config.heartbeatEnabled) { + startHeartbeat(); + } + }; + + ws.value.onmessage = function (event) { + handleIncomingMessage(event); + }; + + ws.value.onclose = function (event) { + clearTimeout(connectionTimeout); + cleanupTimers(); + updateConnectionStatus("disconnected"); + + const reason = getCloseReason(event.code); + addMessage( + `๐Ÿ”ด Disconnected: ${reason}`, + "error", + `Code: ${event.code}, Reason: ${event.reason || "Unknown"}` + ); + + // Auto-reconnect logic + if (!isManualDisconnect && config.autoReconnect) { + scheduleReconnect(); + } + }; + + ws.value.onerror = function (error) { + updateConnectionStatus("error"); + connectionHealth = "poor"; + addMessage("โŒ WebSocket Error occurred", "error", error.toString()); + }; + } catch (error) { + updateConnectionStatus("error"); + addMessage( + "โŒ Failed to create WebSocket connection", + "error", + error instanceof Error ? error.message : String(error) + ); + scheduleReconnect(); + } + }; + + const disconnect = () => { + if (!isBrowser) return; + + isManualDisconnect = true; + cleanupConnection(); + updateConnectionStatus("disconnected"); + addMessage("๐Ÿ”ด Manually disconnected from WebSocket", "info"); + }; + + const sendMessage = (message: any) => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot send message: not connected", "error"); + return; + } + + try { + const messageData = { + type: "message", + data: message, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(messageData)); + connectionState.messagesSent++; + addMessage("๐Ÿ“ค Message sent", "info", message); + } catch (error) { + addMessage( + "โŒ Failed to send message", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const broadcastMessage = (message: string) => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot broadcast message: not connected", "error"); + return; + } + + try { + const messageData = { + type: "broadcast", + message, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(messageData)); + connectionState.messagesSent++; + addMessage("๐Ÿ“ก Message broadcasted", "info", message); + } catch (error) { + addMessage( + "โŒ Failed to broadcast message", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const sendDirectMessage = (clientId: string, message: string) => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot send direct message: not connected", "error"); + return; + } + + try { + const messageData = { + type: "direct_message", + clientId, + message, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(messageData)); + connectionState.messagesSent++; + addMessage(`๐Ÿ“จ Direct message sent to ${clientId}`, "info", message); + } catch (error) { + addMessage( + "โŒ Failed to send direct message", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const sendRoomMessage = (room: string, message: string) => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot send room message: not connected", "error"); + return; + } + + try { + const messageData = { + type: "room_message", + room, + message, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(messageData)); + connectionState.messagesSent++; + addMessage(`๐Ÿ“ข Room message sent to ${room}`, "info", message); + } catch (error) { + addMessage( + "โŒ Failed to send room message", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const getOnlineUsers = () => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot get online users: not connected", "error"); + return; + } + + try { + const message = { + type: "get_online_users", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + } catch (error) { + addMessage( + "โŒ Failed to request online users", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const testConnection = () => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot test connection: not connected", "error"); + return; + } + + try { + const message = { + type: "ping", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + addMessage("๐Ÿ“ Connection test sent", "info"); + } catch (error) { + addMessage( + "โŒ Failed to test connection", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const sendHeartbeat = () => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot send heartbeat: not connected", "error"); + return; + } + + try { + const message = { + type: "ping", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + lastHeartbeatTime = Date.now(); + } catch (error) { + addMessage( + "โŒ Failed to send heartbeat", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const executeDatabaseQuery = async (query: string) => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + throw new Error("Not connected"); + } + + try { + const message = { + type: "database_query", + query, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + return { success: true, message: "Database query sent successfully" }; + } catch (error) { + throw new Error( + `Failed to execute database query: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + }; + + const triggerNotification = async (message: string) => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + throw new Error("Not connected"); + } + + try { + const notificationData = { + type: "notification", + message, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(notificationData)); + return { success: true, message: "Notification sent successfully" }; + } catch (error) { + throw new Error( + `Failed to trigger notification: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + }; + + const getStats = () => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot get stats: not connected", "error"); + return; + } + + try { + const message = { + type: "get_stats", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + } catch (error) { + addMessage( + "โŒ Failed to request stats", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const getMonitoringData = () => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot get monitoring data: not connected", "error"); + return; + } + + try { + const message = { + type: "get_monitoring", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + } catch (error) { + addMessage( + "โŒ Failed to request monitoring data", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const clearMessages = () => { + if (!isBrowser) return; + messages.value = []; + messageCount = 0; + }; + + const clearActivityLog = () => { + if (!isBrowser) return; + activityLog.value = []; + }; + + const getMessagesByType = (type: string) => { + return messages.value.filter((msg) => msg.type === type); + }; + + const getRecentMessages = (count: number = 10) => { + return messages.value.slice(0, count); + }; + + const stopHeartbeat = () => { + if (!isBrowser) return; + + if (heartbeatInterval) { + clearInterval(heartbeatInterval); + heartbeatInterval = null; + } + if (heartbeatTimeout) { + clearTimeout(heartbeatTimeout); + heartbeatTimeout = null; + } + missedHeartbeats = 0; + }; + + const cleanupConnection = () => { + if (!isBrowser) return; + + if (ws.value) { + ws.value.close(); + ws.value = null; + } + cleanupTimers(); + }; + + const cleanupTimers = () => { + if (!isBrowser) return; + + if (reconnectTimeout) { + clearTimeout(reconnectTimeout); + reconnectTimeout = null; + } + stopHeartbeat(); + stopUptimeTimer(); + }; + + const scheduleReconnect = () => { + if (!isBrowser || !config.autoReconnect) return; + + if (reconnectAttempts >= config.maxReconnectAttempts) { + addMessage("โŒ Max reconnection attempts reached", "error"); + return; + } + + reconnectAttempts++; + const delay = Math.min( + config.reconnectDelay * Math.pow(2, reconnectAttempts - 1), + config.maxReconnectDelay + ); + + addMessage( + `๐Ÿ”„ Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${config.maxReconnectAttempts})`, + "info" + ); + + reconnectTimeout = window.setTimeout(() => { + connect(); + }, delay); + }; + + const startHeartbeat = () => { + if (!isBrowser || !config.heartbeatEnabled) return; + + stopHeartbeat(); + + heartbeatInterval = window.setInterval(() => { + if (ws.value && ws.value.readyState === WebSocket.OPEN) { + const heartbeatMessage = { + type: "ping", + timestamp: Date.now(), + }; + ws.value.send(JSON.stringify(heartbeatMessage)); + lastHeartbeatTime = Date.now(); + + // Set timeout for heartbeat response + heartbeatTimeout = window.setTimeout(() => { + missedHeartbeats++; + if (missedHeartbeats >= config.maxMissedHeartbeats) { + addMessage( + "โŒ Heartbeat timeout - connection unhealthy", + "warning" + ); + connectionHealth = "warning"; + } + }, config.heartbeatTimeout); + } + }, config.heartbeatInterval); + }; + + const handleIncomingMessage = (event: MessageEvent) => { + if (!isBrowser) return; + + try { + const data = JSON.parse(event.data); + + // Handle heartbeat response + if (data.type === "pong") { + missedHeartbeats = 0; + connectionHealth = "excellent"; + const latency = Date.now() - lastHeartbeatTime; + connectionState.connectionLatency = latency; + return; + } + + // Handle connection info + if (data.type === "connection_info") { + connectionState.clientId = data.clientId; + connectionState.staticId = data.staticId; + connectionState.currentRoom = data.room; + connectionState.userId = data.userId; + connectionState.ipAddress = data.ipAddress; + return; + } + + // Handle welcome message (server sends connection info in welcome message) + if (data.type === "welcome") { + console.log("Received welcome message:", data); + + // Map server snake_case fields to camelCase for Vue components + if (data.client_id) { + connectionState.clientId = data.client_id; + console.log("Set clientId:", data.client_id); + } + if (data.static_id) { + connectionState.staticId = data.static_id; + console.log("Set staticId:", data.static_id); + } + if (data.room) { + connectionState.currentRoom = data.room; + console.log("Set currentRoom:", data.room); + } + if (data.user_id) { + connectionState.userId = data.user_id; + console.log("Set userId:", data.user_id); + } + if (data.ip_address) { + connectionState.ipAddress = data.ip_address; + console.log("Set ipAddress:", data.ip_address); + } + if (data.connected_at) { + connectionState.connectionStartTime = data.connected_at * 1000; + console.log("Set connectionStartTime:", data.connected_at * 1000); + } + + // Force reactive update by triggering a change + connectionState.connectionHealth = + connectionState.connectionHealth === "good" ? "excellent" : "good"; + + console.log("Updated connectionState:", connectionState); + addMessage( + "๐ŸŸข Connection established successfully", + "success", + "Welcome message processed" + ); + return; + } + + // Handle stats + if (data.type === "stats") { + stats.value = data; + return; + } + + // Handle monitoring data + if (data.type === "monitoring") { + monitoringData.value = data; + return; + } + + // Handle online users + if (data.type === "online_users") { + onlineUsers.value = data.users; + return; + } + + // Handle activity log + if (data.type === "activity") { + activityLog.value.unshift(data); + if (activityLog.value.length > 100) { + activityLog.value = activityLog.value.slice(0, 100); + } + return; + } + + // Handle server info + if (data.type === "server_info") { + serverInfo.value = data; + return; + } + + // Handle system health + if (data.type === "system_health") { + systemHealth.value = data; + return; + } + + // Handle regular messages + addMessage(data.type || "message", data, data.messageId); + } catch (error) { + addMessage( + "โŒ Failed to parse message", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const getCloseReason = (code: number): string => { + switch (code) { + case 1000: + return "Normal closure"; + case 1001: + return "Going away"; + case 1002: + return "Protocol error"; + case 1003: + return "Unsupported data"; + case 1004: + return "Reserved"; + case 1005: + return "No status received"; + case 1006: + return "Abnormal closure"; + case 1007: + return "Invalid frame payload data"; + case 1008: + return "Policy violation"; + case 1009: + return "Message too big"; + case 1010: + return "Mandatory extension"; + case 1011: + return "Internal server error"; + case 1012: + return "Service restart"; + case 1013: + return "Try again later"; + case 1014: + return "Bad gateway"; + case 1015: + return "TLS handshake"; + default: + return "Unknown reason"; + } + }; + + const isMessageLimitReached = computed(() => { + return messages.value.length >= config.maxMessages; + }); + + const shouldShowMessageWarning = computed(() => { + return messages.value.length >= config.messageWarningThreshold; + }); + + return { + // State + ws, + isConnected, + isConnecting, + connectionStatus, + connectionState, + config, + messages, + stats, + monitoringData, + onlineUsers, + activityLog, + + // Admin state + serverInfo, + systemHealth, + + // Methods + connect, + disconnect, + sendMessage, + broadcastMessage, + sendDirectMessage, + sendRoomMessage, + getServerInfo, + getOnlineUsers, + testConnection, + sendHeartbeat, + executeDatabaseQuery, + triggerNotification, + getStats, + getMonitoringData, + executeAdminCommand, + getSystemHealth, + clearMessages, + clearActivityLog, + getMessagesByType, + getRecentMessages, + cleanup, + + // Computed + isMessageLimitReached, + shouldShowMessageWarning, + connectionHealthColor, + connectionHealthText, + }; +}; diff --git a/examples/clientsocket/composables/useWebSocket.fixed b/examples/clientsocket/composables/useWebSocket.fixed new file mode 100644 index 0000000..972b9d5 --- /dev/null +++ b/examples/clientsocket/composables/useWebSocket.fixed @@ -0,0 +1,1073 @@ +import { ref, computed, reactive, type Ref } from "vue"; +import type { + WebSocketMessage, + ConnectionState, + WebSocketConfig, + MessageHistory, + ConnectionStats, + MonitoringData, + ClientInfo, + OnlineUser, + ActivityLog, +} from "../types/websocket"; + +export const useWebSocket = () => { + // Check if we're in browser environment + const isBrowser = process.client; + + // WebSocket connection + const ws: Ref = ref(null); + + // Connection state - consolidated into single reactive object + const connectionState = reactive({ + isConnected: false, + isConnecting: false, + connectionStatus: "disconnected", + clientId: null, + staticId: null, + currentRoom: null, + userId: "anonymous", + ipAddress: null, + connectionStartTime: null, + lastPingTime: null, + connectionLatency: 0, + connectionHealth: "poor", + reconnectAttempts: 0, + messagesReceived: 0, + messagesSent: 0, + uptime: "00:00:00", + }); + + // Configuration + const config = reactive({ + wsUrl: "ws://localhost:8080/api/v1/ws", + userId: "anonymous", + room: "default", + staticId: "", + useIPBasedId: false, + autoReconnect: true, + heartbeatEnabled: true, + maxReconnectAttempts: 10, + reconnectDelay: 1000, + maxReconnectDelay: 30000, + heartbeatInterval: 30000, + heartbeatTimeout: 5000, + maxMissedHeartbeats: 3, + maxMessages: 1000, + messageWarningThreshold: 800, + actionThrottle: 100, + }); + + // Message history and data + const messages = ref([]); + const stats = ref(null); + const monitoringData = ref(null); + const onlineUsers = ref([]); + const activityLog = ref([]); + + // Admin functionality + const serverInfo = ref(null); + const systemHealth = ref(null); + + // Timer references for proper cleanup + let reconnectTimeout: number | null = null; + let heartbeatInterval: number | null = null; + let heartbeatTimeout: number | null = null; + let uptimeInterval: number | null = null; + let connectionStartTime: number | null = null; + + // Heartbeat tracking + let missedHeartbeats = 0; + let lastHeartbeatTime = 0; + let isManualDisconnect = false; + + // Helper function to update connection status + const updateConnectionStatus = ( + status: "disconnected" | "connecting" | "connected" | "error" + ) => { + connectionState.connectionStatus = status; + + switch (status) { + case "connected": + connectionState.isConnected = true; + connectionState.isConnecting = false; + connectionState.connectionHealth = "good"; + break; + case "connecting": + connectionState.isConnected = false; + connectionState.isConnecting = true; + connectionState.connectionHealth = "warning"; + break; + case "disconnected": + case "error": + connectionState.isConnected = false; + connectionState.isConnecting = false; + connectionState.connectionHealth = "poor"; + break; + } + }; + + // Helper function to update uptime + const updateUptime = () => { + if (connectionStartTime && connectionState.isConnected) { + const now = Date.now(); + const uptime = now - connectionStartTime; + const seconds = Math.floor(uptime / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + connectionState.uptime = `${hours.toString().padStart(2, "0")}:${( + minutes % 60 + ) + .toString() + .padStart(2, "0")}:${(seconds % 60).toString().padStart(2, "0")}`; + } else { + connectionState.uptime = "00:00:00"; + } + }; + + // Timer management + const startUptimeTimer = () => { + if (uptimeInterval) { + clearInterval(uptimeInterval); + } + uptimeInterval = window.setInterval(updateUptime, 1000); + }; + + const stopUptimeTimer = () => { + if (uptimeInterval) { + clearInterval(uptimeInterval); + uptimeInterval = null; + } + }; + + // Message handling + const getIconForMessageType = (type: string): string => { + switch (type) { + case "success": + return "๐ŸŸข"; + case "error": + return "โŒ"; + case "warning": + return "โš ๏ธ"; + case "info": + return "โ„น๏ธ"; + default: + return "๐Ÿ“"; + } + }; + + const addMessage = ( + type: string, + data: any, + messageId?: string, + icon?: string + ) => { + if (!isBrowser) return; + + const now = new Date(); + const timeString = now.toLocaleTimeString("en-US", { + hour12: true, + hour: "numeric", + minute: "2-digit", + second: "2-digit", + }); + + // Enhanced message formatting based on type + let formattedData = data; + let displayType = type; + + switch (type) { + case "connection_info": + displayType = "info"; + formattedData = { + client_id: data.clientId, + connected_at: data.connectedAt, + ip_address: data.ipAddress, + last_ping: data.lastPingTime || "N/A", + room: data.room || "default", + static_id: data.staticId || "none", + user_id: data.userId || "anonymous", + }; + break; + + case "heartbeat": + displayType = "info"; + formattedData = `โค๏ธ Heartbeat started\nInterval: ${ + config.heartbeatInterval / 1000 + }s`; + break; + + case "pong": + displayType = "success"; + formattedData = `๐Ÿ“ Pong received\nLatency: ${connectionState.connectionLatency}ms`; + break; + + case "message": + displayType = "info"; + formattedData = `๐Ÿ“จ Message received\n${JSON.stringify(data, null, 2)}`; + break; + + case "broadcast": + displayType = "info"; + formattedData = `๐Ÿ“ก Broadcast received\n${data}`; + break; + + case "direct_message": + displayType = "info"; + formattedData = `๐Ÿ“จ Direct message from ${data.from}\n${data.message}`; + break; + + case "room_message": + displayType = "info"; + formattedData = `๐Ÿ“ข Room message from ${data.room}\n${data.message}`; + break; + + case "stats": + displayType = "info"; + formattedData = `๐Ÿ“Š Stats updated\n${JSON.stringify(data, null, 2)}`; + break; + + case "monitoring": + displayType = "info"; + formattedData = `๐Ÿ“ˆ Monitoring data\n${JSON.stringify(data, null, 2)}`; + break; + + case "online_users": + displayType = "info"; + formattedData = `๐Ÿ‘ฅ Online users updated\nCount: ${ + data.users?.length || 0 + }`; + break; + + case "server_info": + displayType = "info"; + formattedData = `๐Ÿ”ง Server info\n${JSON.stringify(data, null, 2)}`; + break; + + case "system_health": + displayType = "info"; + formattedData = `๐Ÿ’š System health\n${JSON.stringify(data, null, 2)}`; + break; + + default: + if (typeof data === "string") { + formattedData = data; + } else { + formattedData = JSON.stringify(data, null, 2); + } + } + + const message: MessageHistory = { + timestamp: now, + type: displayType, + data: formattedData, + messageId, + size: JSON.stringify(data).length, + icon: icon || getIconForMessageType(displayType), + timeString: timeString, + }; + + messages.value.unshift(message); + + // Keep only the last maxMessages + if (messages.value.length > config.maxMessages) { + messages.value = messages.value.slice(0, config.maxMessages); + } + + // Update connection state + connectionState.messagesReceived++; + }; + + // Computed properties + const connectionHealthColor = computed(() => { + switch (connectionState.connectionHealth) { + case "excellent": + return "#4CAF50"; + case "good": + return "#2196F3"; + case "warning": + return "#FFC107"; + case "poor": + return "#F44336"; + default: + return "#9E9E9E"; + } + }); + + const connectionHealthText = computed(() => { + switch (connectionState.connectionHealth) { + case "excellent": + return "Excellent"; + case "good": + return "Good"; + case "warning": + return "Warning"; + case "poor": + return "Poor"; + default: + return "Unknown"; + } + }); + + const isMessageLimitReached = computed(() => { + return messages.value.length >= config.maxMessages; + }); + + const shouldShowMessageWarning = computed(() => { + return messages.value.length >= config.messageWarningThreshold; + }); + + // Timer cleanup + const cleanupTimers = () => { + if (!isBrowser) return; + + if (reconnectTimeout) { + clearTimeout(reconnectTimeout); + reconnectTimeout = null; + } + if (heartbeatInterval) { + clearInterval(heartbeatInterval); + heartbeatInterval = null; + } + if (heartbeatTimeout) { + clearTimeout(heartbeatTimeout); + heartbeatTimeout = null; + } + stopUptimeTimer(); + }; + + // Connection cleanup + const cleanupConnection = () => { + if (!isBrowser) return; + + if (ws.value) { + ws.value.close(); + ws.value = null; + } + cleanupTimers(); + }; + + // Get close reason description + const getCloseReason = (code: number): string => { + switch (code) { + case 1000: + return "Normal closure"; + case 1001: + return "Going away"; + case 1002: + return "Protocol error"; + case 1003: + return "Unsupported data"; + case 1004: + return "Reserved"; + case 1005: + return "No status received"; + case 1006: + return "Abnormal closure"; + case 1007: + return "Invalid frame payload data"; + case 1008: + return "Policy violation"; + case 1009: + return "Message too big"; + case 1010: + return "Mandatory extension"; + case 1011: + return "Internal server error"; + case 1012: + return "Service restart"; + case 1013: + return "Try again later"; + case 1014: + return "Bad gateway"; + case 1015: + return "TLS handshake"; + default: + return "Unknown reason"; + } + }; + + // Heartbeat management + const stopHeartbeat = () => { + if (!isBrowser) return; + + if (heartbeatInterval) { + clearInterval(heartbeatInterval); + heartbeatInterval = null; + } + if (heartbeatTimeout) { + clearTimeout(heartbeatTimeout); + heartbeatTimeout = null; + } + missedHeartbeats = 0; + }; + + const startHeartbeat = () => { + if (!isBrowser || !config.heartbeatEnabled) return; + + stopHeartbeat(); + + heartbeatInterval = window.setInterval(() => { + if (ws.value && ws.value.readyState === WebSocket.OPEN) { + const heartbeatMessage = { + type: "ping", + timestamp: Date.now(), + }; + ws.value.send(JSON.stringify(heartbeatMessage)); + lastHeartbeatTime = Date.now(); + + // Set timeout for heartbeat response + heartbeatTimeout = window.setTimeout(() => { + missedHeartbeats++; + if (missedHeartbeats >= config.maxMissedHeartbeats) { + addMessage( + "โŒ Heartbeat timeout - connection unhealthy", + "warning" + ); + connectionState.connectionHealth = "warning"; + } + }, config.heartbeatTimeout); + } + }, config.heartbeatInterval); + }; + + // Reconnection logic + const scheduleReconnect = () => { + if (!isBrowser || !config.autoReconnect) return; + + if (connectionState.reconnectAttempts >= config.maxReconnectAttempts) { + addMessage("โŒ Max reconnection attempts reached", "error"); + return; + } + + connectionState.reconnectAttempts++; + const delay = Math.min( + config.reconnectDelay * + Math.pow(2, connectionState.reconnectAttempts - 1), + config.maxReconnectDelay + ); + + addMessage( + `๐Ÿ”„ Reconnecting in ${delay}ms (attempt ${connectionState.reconnectAttempts}/${config.maxReconnectAttempts})`, + "info" + ); + + reconnectTimeout = window.setTimeout(() => { + connect(); + }, delay); + }; + + // Message handling + const handleIncomingMessage = (event: MessageEvent) => { + if (!isBrowser) return; + + try { + const data = JSON.parse(event.data); + + // Handle heartbeat response + if (data.type === "pong") { + missedHeartbeats = 0; + connectionState.connectionHealth = "excellent"; + const latency = Date.now() - lastHeartbeatTime; + connectionState.connectionLatency = latency; + return; + } + + // Handle connection info + if (data.type === "connection_info") { + connectionState.clientId = data.clientId; + connectionState.staticId = data.staticId; + connectionState.currentRoom = data.room; + connectionState.userId = data.userId; + connectionState.ipAddress = data.ipAddress; + return; + } + + // Handle welcome message (server sends connection info in welcome message) + if (data.type === "welcome") { + console.log("Received welcome message:", data); + + // Map server snake_case fields to camelCase for Vue components + if (data.client_id) { + connectionState.clientId = data.client_id; + console.log("Set clientId:", data.client_id); + } + if (data.static_id) { + connectionState.staticId = data.static_id; + console.log("Set staticId:", data.static_id); + } + if (data.room) { + connectionState.currentRoom = data.room; + console.log("Set currentRoom:", data.room); + } + if (data.user_id) { + connectionState.userId = data.user_id; + console.log("Set userId:", data.user_id); + } + if (data.ip_address) { + connectionState.ipAddress = data.ip_address; + console.log("Set ipAddress:", data.ip_address); + } + if (data.connected_at) { + connectionState.connectionStartTime = data.connected_at * 1000; + console.log("Set connectionStartTime:", data.connected_at * 1000); + } + + // Force reactive update by triggering a change + connectionState.connectionHealth = + connectionState.connectionHealth === "good" ? "excellent" : "good"; + + console.log("Updated connectionState:", connectionState); + addMessage( + "๐ŸŸข Connection established successfully", + "success", + "Welcome message processed" + ); + return; + } + + // Handle stats + if (data.type === "stats") { + stats.value = data; + return; + } + + // Handle monitoring data + if (data.type === "monitoring") { + monitoringData.value = data; + return; + } + + // Handle online users + if (data.type === "online_users") { + onlineUsers.value = data.users; + return; + } + + // Handle activity log + if (data.type === "activity") { + activityLog.value.unshift(data); + if (activityLog.value.length > 100) { + activityLog.value = activityLog.value.slice(0, 100); + } + return; + } + + // Handle server info + if (data.type === "server_info") { + serverInfo.value = data; + return; + } + + // Handle system health + if (data.type === "system_health") { + systemHealth.value = data; + return; + } + + // Handle regular messages + addMessage(data.type || "message", data, data.messageId); + } catch (error) { + addMessage( + "โŒ Failed to parse message", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + // WebSocket connection methods + const connect = () => { + if (!isBrowser) return; + + // Prevent multiple connection attempts + if (connectionState.isConnecting || connectionState.isConnected) { + return; + } + + // Clean up existing connection + if (ws.value) { + cleanupConnection(); + } + + updateConnectionStatus("connecting"); + isManualDisconnect = false; + connectionStartTime = Date.now(); + startUptimeTimer(); + + try { + // Build WebSocket URL with parameters + let url = config.wsUrl; + const params = new URLSearchParams(); + + if (config.userId) { + params.append("user_id", config.userId); + } + if (config.room) { + params.append("room", config.room); + } + if (config.useIPBasedId) { + params.append("ip_based", "true"); + } else if (config.staticId) { + params.append("static_id", config.staticId); + } + + if (params.toString()) { + url += "?" + params.toString(); + } + + ws.value = new WebSocket(url); + + // Connection timeout + const connectionTimeout = setTimeout(() => { + if (ws.value && ws.value.readyState === WebSocket.CONNECTING) { + ws.value.close(); + updateConnectionStatus("error"); + addMessage("โŒ Connection timeout after 15 seconds", "error"); + scheduleReconnect(); + } + }, 15000); + + ws.value.onopen = function (event) { + clearTimeout(connectionTimeout); + updateConnectionStatus("connected"); + connectionState.reconnectAttempts = 0; + connectionState.connectionStartTime = connectionStartTime; + + addMessage( + "๐ŸŸข Connected to WebSocket server", + "success", + "Connection established successfully" + ); + + // Start heartbeat if enabled + if (config.heartbeatEnabled) { + startHeartbeat(); + } + }; + + ws.value.onmessage = function (event) { + handleIncomingMessage(event); + }; + + ws.value.onclose = function (event) { + clearTimeout(connectionTimeout); + cleanupTimers(); + updateConnectionStatus("disconnected"); + + const reason = getCloseReason(event.code); + addMessage( + `๐Ÿ”ด Disconnected: ${reason}`, + "error", + `Code: ${event.code}, Reason: ${event.reason || "Unknown"}` + ); + + // Auto-reconnect logic + if (!isManualDisconnect && config.autoReconnect) { + scheduleReconnect(); + } + }; + + ws.value.onerror = function (error) { + updateConnectionStatus("error"); + addMessage("โŒ WebSocket Error occurred", "error", error.toString()); + }; + } catch (error) { + updateConnectionStatus("error"); + addMessage( + "โŒ Failed to create WebSocket connection", + "error", + error instanceof Error ? error.message : String(error) + ); + scheduleReconnect(); + } + }; + + const disconnect = () => { + if (!isBrowser) return; + + isManualDisconnect = true; + cleanupConnection(); + updateConnectionStatus("disconnected"); + addMessage("๐Ÿ”ด Manually disconnected from WebSocket", "info"); + }; + + // Message sending methods + const sendMessage = (message: any) => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot send message: not connected", "error"); + return false; + } + + try { + const messageData = { + type: "message", + data: message, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(messageData)); + connectionState.messagesSent++; + addMessage("๐Ÿ“ค Message sent", "info", message); + return true; + } catch (error) { + addMessage( + "โŒ Failed to send message", + "error", + error instanceof Error ? error.message : String(error) + ); + return false; + } + }; + + const broadcastMessage = (message: string) => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot broadcast message: not connected", "error"); + return false; + } + + try { + const messageData = { + type: "broadcast", + message, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(messageData)); + connectionState.messagesSent++; + addMessage("๐Ÿ“ก Message broadcasted", "info", message); + return true; + } catch (error) { + addMessage( + "โŒ Failed to broadcast message", + "error", + error instanceof Error ? error.message : String(error) + ); + return false; + } + }; + + const sendDirectMessage = (clientId: string, message: string) => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot send direct message: not connected", "error"); + return false; + } + + try { + const messageData = { + type: "direct_message", + clientId, + message, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(messageData)); + connectionState.messagesSent++; + addMessage(`๐Ÿ“จ Direct message sent to ${clientId}`, "info", message); + return true; + } catch (error) { + addMessage( + "โŒ Failed to send direct message", + "error", + error instanceof Error ? error.message : String(error) + ); + return false; + } + }; + + const sendRoomMessage = (room: string, message: string) => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot send room message: not connected", "error"); + return false; + } + + try { + const messageData = { + type: "room_message", + room, + message, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(messageData)); + connectionState.messagesSent++; + addMessage(`๐Ÿ“ข Room message sent to ${room}`, "info", message); + return true; + } catch (error) { + addMessage( + "โŒ Failed to send room message", + "error", + error instanceof Error ? error.message : String(error) + ); + return false; + } + }; + + // Admin and utility methods + const executeAdminCommand = async (command: string, params: any) => { + if (!isBrowser || !ws.value) throw new Error("Not connected"); + + const message = { + type: "admin_command", + command, + params, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + return { success: true, message: "Command sent successfully" }; + }; + + const getServerInfo = async () => { + if (!isBrowser || !ws.value) throw new Error("Not connected"); + + const message = { + type: "get_server_info", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + }; + + const getSystemHealth = async () => { + if (!isBrowser || !ws.value) throw new Error("Not connected"); + + const message = { + type: "get_system_health", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + }; + + const getOnlineUsers = () => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot get online users: not connected", "error"); + return; + } + + try { + const message = { + type: "get_online_users", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + } catch (error) { + addMessage( + "โŒ Failed to request online users", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const testConnection = () => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot test connection: not connected", "error"); + return; + } + + try { + const message = { + type: "ping", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + addMessage("๐Ÿ“ Connection test sent", "info"); + } catch (error) { + addMessage( + "โŒ Failed to test connection", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const sendHeartbeat = () => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot send heartbeat: not connected", "error"); + return; + } + + try { + const message = { + type: "ping", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + lastHeartbeatTime = Date.now(); + } catch (error) { + addMessage( + "โŒ Failed to send heartbeat", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const executeDatabaseQuery = async (query: string) => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + throw new Error("Not connected"); + } + + try { + const message = { + type: "database_query", + query, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + return { success: true, message: "Database query sent successfully" }; + } catch (error) { + throw new Error( + `Failed to execute database query: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + }; + + const triggerNotification = async (message: string) => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + throw new Error("Not connected"); + } + + try { + const notificationData = { + type: "notification", + message, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(notificationData)); + return { success: true, message: "Notification sent successfully" }; + } catch (error) { + throw new Error( + `Failed to trigger notification: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + }; + + const getStats = () => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot get stats: not connected", "error"); + return; + } + + try { + const message = { + type: "get_stats", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + } catch (error) { + addMessage( + "โŒ Failed to request stats", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const getMonitoringData = () => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot get monitoring data: not connected", "error"); + return; + } + + try { + const message = { + type: "get_monitoring", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + } catch (error) { + addMessage( + "โŒ Failed to request monitoring data", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + // Utility methods + const clearMessages = () => { + if (!isBrowser) return; + messages.value = []; + }; + + const clearActivityLog = () => { + if (!isBrowser) return; + activityLog.value = []; + }; + + const getMessagesByType = (type: string) => { + return messages.value.filter((msg) => msg.type === type); + }; + + const getRecentMessages = (count: number = 10) => { + return messages.value.slice(0, count); + }; + + // Cleanup on unmount + const cleanup = () => { + if (!isBrowser) return; + + disconnect(); + cleanupTimers(); + }; + + return { + // State + ws, + connectionState, + config, + messages, + stats, + monitoringData, + onlineUsers, + activityLog, + + // Admin state + serverInfo, + systemHealth, + + // Methods + connect, + disconnect, + sendMessage, + broadcastMessage, + sendDirectMessage, + sendRoomMessage, + getServerInfo, + getOnlineUsers, + testConnection, + sendHeartbeat, + executeDatabaseQuery, + triggerNotification, + getStats, + getMonitoringData, + executeAdminCommand, + getSystemHealth, + clearMessages, + clearActivityLog, + getMessagesByType, + getRecentMessages, + cleanup, + + // Computed + isMessageLimitReached, + shouldShowMessageWarning, + connectionHealthColor, + connectionHealthText, + }; +}; diff --git a/examples/clientsocket/composables/useWebSocket.ts b/examples/clientsocket/composables/useWebSocket.ts new file mode 100644 index 0000000..7478c16 --- /dev/null +++ b/examples/clientsocket/composables/useWebSocket.ts @@ -0,0 +1,1073 @@ +import { ref, computed, reactive, nextTick } from "vue"; +import type { + WebSocketMessage, + ConnectionState, + WebSocketConfig, + MessageHistory, + ConnectionStats, + MonitoringData, + ClientInfo, + OnlineUser, + ActivityLog, +} from "../types/websocket"; + +export const useWebSocket = () => { + // Check if we're in browser environment + const isBrowser = process.client; + + const ws = ref(null); + const isConnected = ref(false); + const isConnecting = ref(false); + const connectionStatus = ref< + "disconnected" | "connecting" | "connected" | "error" + >("disconnected"); + + const connectionState = reactive({ + isConnected: false, + isConnecting: false, + connectionStatus: "disconnected", + clientId: null, + staticId: null, + currentRoom: null, + userId: "anonymous", + ipAddress: null, + connectionStartTime: null, + lastPingTime: null, + connectionLatency: 0, + connectionHealth: "poor", + reconnectAttempts: 0, + messagesReceived: 0, + messagesSent: 0, + uptime: "00:00:00", + }); + + const config = reactive({ + wsUrl: "ws://localhost:8080/api/v1/ws", + userId: "anonymous", + room: "default", + staticId: "", + useIPBasedId: false, + autoReconnect: true, + heartbeatEnabled: true, + maxReconnectAttempts: 10, + reconnectDelay: 1000, + maxReconnectDelay: 30000, + heartbeatInterval: 30000, + heartbeatTimeout: 5000, + maxMissedHeartbeats: 3, + maxMessages: 1000, + messageWarningThreshold: 800, + actionThrottle: 100, + }); + + const messages = ref([]); + const stats = ref(null); + const monitoringData = ref(null); + const onlineUsers = ref([]); + const activityLog = ref([]); + + let reconnectTimeout: number | null = null; + let heartbeatInterval: number | null = null; + let heartbeatTimeout: number | null = null; + let missedHeartbeats = 0; + let lastHeartbeatTime = 0; + let messageCount = 0; + let connectionStartTime: number | null = null; + let uptimeInterval: number | null = null; + let isManualDisconnect = false; + let reconnectAttempts = 0; + let connectionHealth: "excellent" | "good" | "warning" | "poor" = "poor"; + + const updateConnectionStatus = ( + status: "disconnected" | "connecting" | "connected" | "error" + ) => { + connectionStatus.value = status; + connectionState.connectionStatus = status; + + switch (status) { + case "connected": + isConnected.value = true; + isConnecting.value = false; + connectionState.isConnected = true; + connectionState.isConnecting = false; + connectionState.connectionHealth = "good"; + break; + case "connecting": + isConnected.value = false; + isConnecting.value = true; + connectionState.isConnected = false; + connectionState.isConnecting = true; + connectionState.connectionHealth = "warning"; + break; + case "disconnected": + isConnected.value = false; + isConnecting.value = false; + connectionState.isConnected = false; + connectionState.isConnecting = false; + connectionState.connectionHealth = "poor"; + break; + case "error": + isConnected.value = false; + isConnecting.value = false; + connectionState.isConnected = false; + connectionState.isConnecting = false; + connectionState.connectionHealth = "poor"; + break; + } + }; + + const updateUptime = () => { + if (connectionStartTime && isConnected.value) { + const now = Date.now(); + const uptime = now - connectionStartTime; + const seconds = Math.floor(uptime / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + connectionState.uptime = `${hours.toString().padStart(2, "0")}:${( + minutes % 60 + ) + .toString() + .padStart(2, "0")}:${(seconds % 60).toString().padStart(2, "0")}`; + } else { + connectionState.uptime = "00:00:00"; + } + }; + + const startUptimeTimer = () => { + if (uptimeInterval) { + clearInterval(uptimeInterval); + } + uptimeInterval = window.setInterval(updateUptime, 1000); + }; + + const stopUptimeTimer = () => { + if (uptimeInterval) { + clearInterval(uptimeInterval); + uptimeInterval = null; + } + }; + + // Only run WebSocket logic in browser + if (isBrowser) { + // Initialize connection state + updateConnectionStatus("disconnected"); + } + + const addMessage = ( + type: string, + data: any, + messageId?: string, + icon?: string + ) => { + if (!isBrowser) return; + + const now = new Date(); + const timeString = now.toLocaleTimeString("en-US", { + hour12: true, + hour: "numeric", + minute: "2-digit", + second: "2-digit", + }); + + // Enhanced message formatting based on type + let formattedData = data; + let displayType = type; + + switch (type) { + case "connection_info": + displayType = "info"; + formattedData = { + client_id: data.clientId, + connected_at: data.connectedAt, + ip_address: data.ipAddress, + last_ping: data.lastPingTime || "N/A", + room: data.room || "default", + static_id: data.staticId || "none", + user_id: data.userId || "anonymous", + }; + break; + + case "heartbeat": + displayType = "info"; + formattedData = `โค๏ธ Heartbeat started\nInterval: ${ + config.heartbeatInterval / 1000 + }s`; + break; + + case "pong": + displayType = "success"; + formattedData = `๐Ÿ“ Pong received\nLatency: ${connectionState.connectionLatency}ms`; + break; + + case "message": + displayType = "info"; + formattedData = `๐Ÿ“จ Message received\n${JSON.stringify(data, null, 2)}`; + break; + + case "broadcast": + displayType = "info"; + formattedData = `๐Ÿ“ก Broadcast received\n${data}`; + break; + + case "direct_message": + displayType = "info"; + formattedData = `๐Ÿ“จ Direct message from ${data.from}\n${data.message}`; + break; + + case "room_message": + displayType = "info"; + formattedData = `๐Ÿ“ข Room message from ${data.room}\n${data.message}`; + break; + + case "stats": + displayType = "info"; + formattedData = `๐Ÿ“Š Stats updated\n${JSON.stringify(data, null, 2)}`; + break; + + case "monitoring": + displayType = "info"; + formattedData = `๐Ÿ“ˆ Monitoring data\n${JSON.stringify(data, null, 2)}`; + break; + + case "online_users": + displayType = "info"; + formattedData = `๐Ÿ‘ฅ Online users updated\nCount: ${ + data.users?.length || 0 + }`; + break; + + case "server_info": + displayType = "info"; + formattedData = `๐Ÿ”ง Server info\n${JSON.stringify(data, null, 2)}`; + break; + + case "system_health": + displayType = "info"; + formattedData = `๐Ÿ’š System health\n${JSON.stringify(data, null, 2)}`; + break; + + default: + if (typeof data === "string") { + formattedData = data; + } else { + formattedData = JSON.stringify(data, null, 2); + } + } + + const message: MessageHistory = { + timestamp: now, + type: displayType, + data: formattedData, + messageId, + size: JSON.stringify(data).length, + icon: icon || getIconForMessageType(displayType), + timeString: timeString, + }; + + messages.value.unshift(message); + messageCount++; + + // Keep only the last maxMessages + if (messages.value.length > config.maxMessages) { + messages.value = messages.value.slice(0, config.maxMessages); + } + + // Update connection state + connectionState.messagesReceived++; + }; + + const getIconForMessageType = (type: string): string => { + switch (type) { + case "success": + return "๐ŸŸข"; + case "error": + return "โŒ"; + case "warning": + return "โš ๏ธ"; + case "info": + return "โ„น๏ธ"; + default: + return "๐Ÿ“"; + } + }; + + const connectionHealthColor = computed(() => { + switch (connectionState.connectionHealth) { + case "excellent": + return "#4CAF50"; + case "good": + return "#2196F3"; + case "warning": + return "#FFC107"; + case "poor": + return "#F44336"; + default: + return "#9E9E9E"; + } + }); + + const connectionHealthText = computed(() => { + switch (connectionState.connectionHealth) { + case "excellent": + return "Excellent"; + case "good": + return "Good"; + case "warning": + return "Warning"; + case "poor": + return "Poor"; + default: + return "Unknown"; + } + }); + + // Admin functionality + const serverInfo = ref(null); + const systemHealth = ref(null); + + const executeAdminCommand = async (command: string, params: any) => { + if (!isBrowser || !ws.value) throw new Error("Not connected"); + + const message = { + type: "admin_command", + command, + params, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + return { success: true, message: "Command sent successfully" }; + }; + + const getServerInfo = async () => { + if (!isBrowser || !ws.value) throw new Error("Not connected"); + + const message = { + type: "get_server_info", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + }; + + const getSystemHealth = async () => { + if (!isBrowser || !ws.value) throw new Error("Not connected"); + + const message = { + type: "get_system_health", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + }; + + // Cleanup on unmount + const cleanup = () => { + if (!isBrowser) return; + + disconnect(); + if (reconnectTimeout) { + clearTimeout(reconnectTimeout); + } + stopHeartbeat(); + }; + + // WebSocket connection methods (only available in browser) + const connect = () => { + if (!isBrowser) return; + + // Prevent multiple connection attempts + if (isConnecting.value || isConnected.value) { + return; + } + + // Clean up existing connection + if (ws.value) { + cleanupConnection(); + } + + updateConnectionStatus("connecting"); + isManualDisconnect = false; + connectionStartTime = Date.now(); + startUptimeTimer(); + + try { + // Build WebSocket URL with parameters + let url = config.wsUrl; + const params = new URLSearchParams(); + + if (config.userId) { + params.append("user_id", config.userId); + } + if (config.room) { + params.append("room", config.room); + } + if (config.useIPBasedId) { + params.append("ip_based", "true"); + } else if (config.staticId) { + params.append("static_id", config.staticId); + } + + if (params.toString()) { + url += "?" + params.toString(); + } + + ws.value = new WebSocket(url); + + // Connection timeout + const connectionTimeout = setTimeout(() => { + if (ws.value && ws.value.readyState === WebSocket.CONNECTING) { + ws.value.close(); + updateConnectionStatus("error"); + addMessage("โŒ Connection timeout after 15 seconds", "error"); + scheduleReconnect(); + } + }, 15000); + + ws.value.onopen = function (event) { + clearTimeout(connectionTimeout); + updateConnectionStatus("connected"); + reconnectAttempts = 0; + connectionHealth = "good"; + connectionState.connectionStartTime = connectionStartTime; + + addMessage( + "๐ŸŸข Connected to WebSocket server", + "success", + "Connection established successfully" + ); + + // Start heartbeat if enabled + if (config.heartbeatEnabled) { + startHeartbeat(); + } + }; + + ws.value.onmessage = function (event) { + handleIncomingMessage(event); + }; + + ws.value.onclose = function (event) { + clearTimeout(connectionTimeout); + cleanupTimers(); + updateConnectionStatus("disconnected"); + + const reason = getCloseReason(event.code); + addMessage( + `๐Ÿ”ด Disconnected: ${reason}`, + "error", + `Code: ${event.code}, Reason: ${event.reason || "Unknown"}` + ); + + // Auto-reconnect logic + if (!isManualDisconnect && config.autoReconnect) { + scheduleReconnect(); + } + }; + + ws.value.onerror = function (error) { + updateConnectionStatus("error"); + connectionHealth = "poor"; + addMessage("โŒ WebSocket Error occurred", "error", error.toString()); + }; + } catch (error) { + updateConnectionStatus("error"); + addMessage( + "โŒ Failed to create WebSocket connection", + "error", + error instanceof Error ? error.message : String(error) + ); + scheduleReconnect(); + } + }; + + const disconnect = () => { + if (!isBrowser) return; + + isManualDisconnect = true; + cleanupConnection(); + updateConnectionStatus("disconnected"); + addMessage("๐Ÿ”ด Manually disconnected from WebSocket", "info"); + }; + + const sendMessage = (message: any) => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot send message: not connected", "error"); + return; + } + + try { + const messageData = { + type: "message", + data: message, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(messageData)); + connectionState.messagesSent++; + addMessage("๐Ÿ“ค Message sent", "info", message); + } catch (error) { + addMessage( + "โŒ Failed to send message", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const broadcastMessage = (message: string) => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot broadcast message: not connected", "error"); + return; + } + + try { + const messageData = { + type: "broadcast", + message, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(messageData)); + connectionState.messagesSent++; + addMessage("๐Ÿ“ก Message broadcasted", "info", message); + } catch (error) { + addMessage( + "โŒ Failed to broadcast message", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const sendDirectMessage = (clientId: string, message: string) => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot send direct message: not connected", "error"); + return; + } + + try { + const messageData = { + type: "direct_message", + clientId, + message, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(messageData)); + connectionState.messagesSent++; + addMessage(`๐Ÿ“จ Direct message sent to ${clientId}`, "info", message); + } catch (error) { + addMessage( + "โŒ Failed to send direct message", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const sendRoomMessage = (room: string, message: string) => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot send room message: not connected", "error"); + return; + } + + try { + const messageData = { + type: "room_message", + room, + message, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(messageData)); + connectionState.messagesSent++; + addMessage(`๐Ÿ“ข Room message sent to ${room}`, "info", message); + } catch (error) { + addMessage( + "โŒ Failed to send room message", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const getOnlineUsers = () => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot get online users: not connected", "error"); + return; + } + + try { + const message = { + type: "get_online_users", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + } catch (error) { + addMessage( + "โŒ Failed to request online users", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const testConnection = () => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot test connection: not connected", "error"); + return; + } + + try { + const message = { + type: "ping", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + addMessage("๐Ÿ“ Connection test sent", "info"); + } catch (error) { + addMessage( + "โŒ Failed to test connection", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const sendHeartbeat = () => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot send heartbeat: not connected", "error"); + return; + } + + try { + const message = { + type: "ping", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + lastHeartbeatTime = Date.now(); + } catch (error) { + addMessage( + "โŒ Failed to send heartbeat", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const executeDatabaseQuery = async (query: string) => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + throw new Error("Not connected"); + } + + try { + const message = { + type: "database_query", + query, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + return { success: true, message: "Database query sent successfully" }; + } catch (error) { + throw new Error( + `Failed to execute database query: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + }; + + const triggerNotification = async (message: string) => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + throw new Error("Not connected"); + } + + try { + const notificationData = { + type: "notification", + message, + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(notificationData)); + return { success: true, message: "Notification sent successfully" }; + } catch (error) { + throw new Error( + `Failed to trigger notification: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + }; + + const getStats = () => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot get stats: not connected", "error"); + return; + } + + try { + const message = { + type: "get_stats", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + } catch (error) { + addMessage( + "โŒ Failed to request stats", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const getMonitoringData = () => { + if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { + addMessage("โŒ Cannot get monitoring data: not connected", "error"); + return; + } + + try { + const message = { + type: "get_monitoring", + timestamp: Date.now(), + }; + + ws.value.send(JSON.stringify(message)); + } catch (error) { + addMessage( + "โŒ Failed to request monitoring data", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const clearMessages = () => { + if (!isBrowser) return; + messages.value = []; + messageCount = 0; + }; + + const clearActivityLog = () => { + if (!isBrowser) return; + activityLog.value = []; + }; + + const getMessagesByType = (type: string) => { + return messages.value.filter((msg) => msg.type === type); + }; + + const getRecentMessages = (count: number = 10) => { + return messages.value.slice(0, count); + }; + + const stopHeartbeat = () => { + if (!isBrowser) return; + + if (heartbeatInterval) { + clearInterval(heartbeatInterval); + heartbeatInterval = null; + } + if (heartbeatTimeout) { + clearTimeout(heartbeatTimeout); + heartbeatTimeout = null; + } + missedHeartbeats = 0; + }; + + const cleanupConnection = () => { + if (!isBrowser) return; + + if (ws.value) { + ws.value.close(); + ws.value = null; + } + cleanupTimers(); + }; + + const cleanupTimers = () => { + if (!isBrowser) return; + + if (reconnectTimeout) { + clearTimeout(reconnectTimeout); + reconnectTimeout = null; + } + stopHeartbeat(); + stopUptimeTimer(); + }; + + const scheduleReconnect = () => { + if (!isBrowser || !config.autoReconnect) return; + + if (reconnectAttempts >= config.maxReconnectAttempts) { + addMessage("โŒ Max reconnection attempts reached", "error"); + return; + } + + reconnectAttempts++; + const delay = Math.min( + config.reconnectDelay * Math.pow(2, reconnectAttempts - 1), + config.maxReconnectDelay + ); + + addMessage( + `๐Ÿ”„ Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${config.maxReconnectAttempts})`, + "info" + ); + + reconnectTimeout = window.setTimeout(() => { + connect(); + }, delay); + }; + + const startHeartbeat = () => { + if (!isBrowser || !config.heartbeatEnabled) return; + + stopHeartbeat(); + + heartbeatInterval = window.setInterval(() => { + if (ws.value && ws.value.readyState === WebSocket.OPEN) { + const heartbeatMessage = { + type: "ping", + timestamp: Date.now(), + }; + ws.value.send(JSON.stringify(heartbeatMessage)); + lastHeartbeatTime = Date.now(); + + // Set timeout for heartbeat response + heartbeatTimeout = window.setTimeout(() => { + missedHeartbeats++; + if (missedHeartbeats >= config.maxMissedHeartbeats) { + addMessage( + "โŒ Heartbeat timeout - connection unhealthy", + "warning" + ); + connectionHealth = "warning"; + } + }, config.heartbeatTimeout); + } + }, config.heartbeatInterval); + }; + + const handleIncomingMessage = (event: MessageEvent) => { + if (!isBrowser) return; + + try { + const data = JSON.parse(event.data); + + // Handle heartbeat response + if (data.type === "pong") { + missedHeartbeats = 0; + connectionHealth = "excellent"; + const latency = Date.now() - lastHeartbeatTime; + connectionState.connectionLatency = latency; + return; + } + + // Handle connection info + if (data.type === "connection_info") { + connectionState.clientId = data.clientId; + connectionState.staticId = data.staticId; + connectionState.currentRoom = data.room; + connectionState.userId = data.userId; + connectionState.ipAddress = data.ipAddress; + return; + } + + // Handle welcome message (server sends connection info in welcome message) + if (data.type === "welcome") { + console.log("Received welcome message:", data); + + // Map server snake_case fields to camelCase for Vue components + if (data.client_id) { + connectionState.clientId = data.client_id; + console.log("Set clientId:", data.client_id); + } + if (data.static_id) { + connectionState.staticId = data.static_id; + console.log("Set staticId:", data.static_id); + } + if (data.room) { + connectionState.currentRoom = data.room; + console.log("Set currentRoom:", data.room); + } + if (data.user_id) { + connectionState.userId = data.user_id; + console.log("Set userId:", data.user_id); + } + if (data.ip_address) { + connectionState.ipAddress = data.ip_address; + console.log("Set ipAddress:", data.ip_address); + } + if (data.connected_at) { + connectionState.connectionStartTime = data.connected_at * 1000; + console.log("Set connectionStartTime:", data.connected_at * 1000); + } + + // Force reactive update by triggering a change + connectionState.connectionHealth = + connectionState.connectionHealth === "good" ? "excellent" : "good"; + + console.log("Updated connectionState:", connectionState); + addMessage( + "๐ŸŸข Connection established successfully", + "success", + "Welcome message processed" + ); + return; + } + + // Handle stats + if (data.type === "stats") { + stats.value = data; + return; + } + + // Handle monitoring data + if (data.type === "monitoring") { + monitoringData.value = data; + return; + } + + // Handle online users + if (data.type === "online_users") { + onlineUsers.value = data.users; + return; + } + + // Handle activity log + if (data.type === "activity") { + activityLog.value.unshift(data); + if (activityLog.value.length > 100) { + activityLog.value = activityLog.value.slice(0, 100); + } + return; + } + + // Handle server info + if (data.type === "server_info") { + serverInfo.value = data; + return; + } + + // Handle system health + if (data.type === "system_health") { + systemHealth.value = data; + return; + } + + // Handle regular messages + addMessage(data.type || "message", data, data.messageId); + } catch (error) { + addMessage( + "โŒ Failed to parse message", + "error", + error instanceof Error ? error.message : String(error) + ); + } + }; + + const getCloseReason = (code: number): string => { + switch (code) { + case 1000: + return "Normal closure"; + case 1001: + return "Going away"; + case 1002: + return "Protocol error"; + case 1003: + return "Unsupported data"; + case 1004: + return "Reserved"; + case 1005: + return "No status received"; + case 1006: + return "Abnormal closure"; + case 1007: + return "Invalid frame payload data"; + case 1008: + return "Policy violation"; + case 1009: + return "Message too big"; + case 1010: + return "Mandatory extension"; + case 1011: + return "Internal server error"; + case 1012: + return "Service restart"; + case 1013: + return "Try again later"; + case 1014: + return "Bad gateway"; + case 1015: + return "TLS handshake"; + default: + return "Unknown reason"; + } + }; + + const isMessageLimitReached = computed(() => { + return messages.value.length >= config.maxMessages; + }); + + const shouldShowMessageWarning = computed(() => { + return messages.value.length >= config.messageWarningThreshold; + }); + + return { + // State + ws, + isConnected, + isConnecting, + connectionStatus, + connectionState, + config, + messages, + stats, + monitoringData, + onlineUsers, + activityLog, + + // Admin state + serverInfo, + systemHealth, + + // Methods + connect, + disconnect, + sendMessage, + broadcastMessage, + sendDirectMessage, + sendRoomMessage, + getServerInfo, + getOnlineUsers, + testConnection, + sendHeartbeat, + executeDatabaseQuery, + triggerNotification, + getStats, + getMonitoringData, + executeAdminCommand, + getSystemHealth, + clearMessages, + clearActivityLog, + getMessagesByType, + getRecentMessages, + cleanup, + + // Computed + isMessageLimitReached, + shouldShowMessageWarning, + connectionHealthColor, + connectionHealthText, + }; +}; diff --git a/examples/clientsocket/composables/useWebSocket.ts.backup b/examples/clientsocket/composables/useWebSocket.ts.backup new file mode 100644 index 0000000..e90e363 --- /dev/null +++ b/examples/clientsocket/composables/useWebSocket.ts.backup @@ -0,0 +1,318 @@ +import { ref, computed, reactive, nextTick } from 'vue' +import type { + WebSocketMessage, + ConnectionState, + WebSocketConfig, + MessageHistory, + ConnectionStats, + MonitoringData, + ClientInfo, + OnlineUser, + ActivityLog +} from '../types/websocket' + +export const useWebSocket = () => { + // Check if we're in browser environment + const isBrowser = process.client + + const ws = ref(null) + const isConnected = ref(false) + const isConnecting = ref(false) + const connectionStatus = ref<'disconnected' | 'connecting' | 'connected' | 'error'>('disconnected') + + const connectionState = reactive({ + isConnected: false, + isConnecting: false, + connectionStatus: 'disconnected', + clientId: null, + staticId: null, + currentRoom: null, + userId: 'anonymous', + ipAddress: null, + connectionStartTime: null, + lastPingTime: null, + connectionLatency: 0, + connectionHealth: 'poor', + reconnectAttempts: 0, + messagesReceived: 0, + messagesSent: 0, + uptime: '00:00:00' + }) + + const config = reactive({ + wsUrl: 'ws://localhost:8080/api/v1/ws', + userId: 'anonymous', + room: 'default', + staticId: '', + useIPBasedId: false, + autoReconnect: true, + heartbeatEnabled: true, + maxReconnectAttempts: 10, + reconnectDelay: 1000, + maxReconnectDelay: 30000, + heartbeatInterval: 30000, + heartbeatTimeout: 5000, + maxMissedHeartbeats: 3, + maxMessages: 1000, + messageWarningThreshold: 800, + actionThrottle: 100 + }) + + const messages = ref([]) + const stats = ref(null) + const monitoringData = ref(null) + const onlineUsers = ref([]) + const activityLog = ref([]) + + let reconnectTimeout: number | null = null + let heartbeatInterval: number | null = null + let heartbeatTimeout: number | null = null + let missedHeartbeats = 0 + let lastHeartbeatTime = 0 + let messageCount = 0 + + // Only run WebSocket logic in browser + if (isBrowser) { + // WebSocket connection logic here + } + + const addMessage = (type: string, data: any, messageId?: string) => { + if (!isBrowser) return + + const message: MessageHistory = { + timestamp: new Date(), + type, + data, + messageId, + size: JSON.stringify(data).length + } + + messages.value.unshift(message) + messageCount++ + + // Keep only the last maxMessages + if (messages.value.length > config.maxMessages) { + messages.value = messages.value.slice(0, config.maxMessages) + } + + // Update connection state + connectionState.messagesReceived++ + } + + const connectionHealthColor = computed(() => { + switch (connectionState.connectionHealth) { + case 'excellent': return '#4CAF50' + case 'good': return '#2196F3' + case 'warning': return '#FFC107' + case 'poor': return '#F44336' + default: return '#9E9E9E' + } + }) + + const connectionHealthText = computed(() => { + switch (connectionState.connectionHealth) { + case 'excellent': return 'Excellent' + case 'good': return 'Good' + case 'warning': return 'Warning' + case 'poor': return 'Poor' + default: return 'Unknown' + } + }) + + // Admin functionality + const serverInfo = ref(null) + const systemHealth = ref(null) + + const executeAdminCommand = async (command: string, params: any) => { + if (!isBrowser || !ws.value) throw new Error('Not connected') + + const message = { + type: 'admin_command', + command, + params, + timestamp: Date.now() + } + + ws.value.send(JSON.stringify(message)) + return { success: true, message: 'Command sent successfully' } + } + + const getServerInfo = async () => { + if (!isBrowser || !ws.value) throw new Error('Not connected') + + const message = { + type: 'get_server_info', + timestamp: Date.now() + } + + ws.value.send(JSON.stringify(message)) + } + + const getSystemHealth = async () => { + if (!isBrowser || !ws.value) throw new Error('Not connected') + + const message = { + type: 'get_system_health', + timestamp: Date.now() + } + + ws.value.send(JSON.stringify(message)) + } + + // Cleanup on unmount + const cleanup = () => { + if (!isBrowser) return + + disconnect() + if (reconnectTimeout) { + clearTimeout(reconnectTimeout) + } + stopHeartbeat() + } + + // WebSocket connection methods (only available in browser) + const connect = () => { + if (!isBrowser) return + // WebSocket connection logic + } + + const disconnect = () => { + if (!isBrowser) return + // WebSocket disconnection logic + } + + const sendMessage = (message: any) => { + if (!isBrowser || !ws.value) return + // Send message logic + } + + const broadcastMessage = (message: string) => { + if (!isBrowser || !ws.value) return + // Broadcast message logic + } + + const sendDirectMessage = (clientId: string, message: string) => { + if (!isBrowser || !ws.value) return + // Direct message logic + } + + const sendRoomMessage = (room: string, message: string) => { + if (!isBrowser || !ws.value) return + // Room message logic + } + + const getOnlineUsers = () => { + if (!isBrowser || !ws.value) return + // Get online users logic + } + + const testConnection = () => { + if (!isBrowser || !ws.value) return + // Test connection logic + } + + const sendHeartbeat = () => { + if (!isBrowser || !ws.value) return + // Send heartbeat logic + } + + const executeDatabaseQuery = async (query: string) => { + if (!isBrowser || !ws.value) return + // Database query logic + } + + const triggerNotification = async (message: string) => { + if (!isBrowser || !ws.value) return + // Notification logic + } + + const getStats = () => { + if (!isBrowser || !ws.value) return + // Get stats logic + } + + const getMonitoringData = () => { + if (!isBrowser || !ws.value) return + // Get monitoring data logic + } + + const clearMessages = () => { + if (!isBrowser) return + messages.value = [] + messageCount = 0 + } + + const clearActivityLog = () => { + if (!isBrowser) return + activityLog.value = [] + } + + const getMessagesByType = (type: string) => { + return messages.value.filter(msg => msg.type === type) + } + + const getRecentMessages = (count: number = 10) => { + return messages.value.slice(0, count) + } + + const stopHeartbeat = () => { + if (!isBrowser) return + // Stop heartbeat logic + } + + const isMessageLimitReached = computed(() => { + return messages.value.length >= config.maxMessages + }) + + const shouldShowMessageWarning = computed(() => { + return messages.value.length >= config.messageWarningThreshold + }) + + return { + // State + ws, + isConnected, + isConnecting, + connectionStatus, + connectionState, + config, + messages, + stats, + monitoringData, + onlineUsers, + activityLog, + + // Admin state + serverInfo, + systemHealth, + + // Methods + connect, + disconnect, + sendMessage, + broadcastMessage, + sendDirectMessage, + sendRoomMessage, + getServerInfo, + getOnlineUsers, + testConnection, + sendHeartbeat, + executeDatabaseQuery, + triggerNotification, + getStats, + getMonitoringData, + executeAdminCommand, + getSystemHealth, + clearMessages, + clearActivityLog, + getMessagesByType, + getRecentMessages, + cleanup, + + // Computed + isMessageLimitReached, + shouldShowMessageWarning, + connectionHealthColor, + connectionHealthText + } +} diff --git a/examples/clientsocket/nuxt.config.ts b/examples/clientsocket/nuxt.config.ts new file mode 100644 index 0000000..dd716db --- /dev/null +++ b/examples/clientsocket/nuxt.config.ts @@ -0,0 +1,22 @@ +export default defineNuxtConfig({ + devtools: { enabled: true }, + modules: [], + css: ['~/assets/css/main.css', 'vuetify/styles'], + build: { + transpile: ['vuetify'] + }, + runtimeConfig: { + public: { + wsUrl: 'ws://localhost:8080/api/v1/ws' + } + }, + typescript: { + typeCheck: false + }, + vite: { + define: { + global: 'globalThis' + } + }, + compatibilityDate: '2024-04-03' +}) diff --git a/examples/clientsocket/package-lock.json b/examples/clientsocket/package-lock.json new file mode 100644 index 0000000..2723d6d --- /dev/null +++ b/examples/clientsocket/package-lock.json @@ -0,0 +1,11504 @@ +{ + "name": "nuxt3-websocket-client", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nuxt3-websocket-client", + "hasInstallScript": true, + "dependencies": { + "@mdi/font": "^7.3.67", + "@nuxtjs/vuetify": "^1.12.3", + "highlight.js": "^11.9.0", + "pinia": "^2.1.7", + "vue3-highlightjs": "^1.0.5", + "vuetify": "^3.4.0" + }, + "devDependencies": { + "@nuxt/devtools": "latest", + "nuxt": "^3.8.0", + "vue": "^3.3.8", + "vue-router": "^4.2.5", + "vue-tsc": "^3.0.8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", + "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz", + "integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "mime": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", + "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", + "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.0.tgz", + "integrity": "sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "consola": "^3.2.3", + "detect-libc": "^2.0.0", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^2.6.7", + "nopt": "^8.0.0", + "semver": "^7.5.3", + "tar": "^7.4.0" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/detect-libc": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz", + "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@mdi/font": { + "version": "7.4.47", + "resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz", + "integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==", + "license": "Apache-2.0" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.5.tgz", + "integrity": "sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nuxt/cli": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@nuxt/cli/-/cli-3.28.0.tgz", + "integrity": "sha512-WQ751WxWLBIeH3TDFt/LWQ2znyAKxpR5+gpv80oerwnVQs4GKajAfR6dIgExXZkjaPUHEFv2lVD9vM+frbprzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "c12": "^3.2.0", + "citty": "^0.1.6", + "clipboardy": "^4.0.0", + "confbox": "^0.2.2", + "consola": "^3.4.2", + "defu": "^6.1.4", + "exsolve": "^1.0.7", + "fuse.js": "^7.1.0", + "get-port-please": "^3.2.0", + "giget": "^2.0.0", + "h3": "^1.15.4", + "httpxy": "^0.1.7", + "jiti": "^2.5.1", + "listhen": "^1.9.0", + "nypm": "^0.6.1", + "ofetch": "^1.4.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "scule": "^1.3.0", + "semver": "^7.7.2", + "std-env": "^3.9.0", + "tinyexec": "^1.0.1", + "ufo": "^1.6.1", + "youch": "^4.1.0-beta.11" + }, + "bin": { + "nuxi": "bin/nuxi.mjs", + "nuxi-ng": "bin/nuxi.mjs", + "nuxt": "bin/nuxi.mjs", + "nuxt-cli": "bin/nuxi.mjs" + }, + "engines": { + "node": "^16.10.0 || >=18.0.0" + } + }, + "node_modules/@nuxt/cli/node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nuxt/devalue": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@nuxt/devalue/-/devalue-2.0.2.tgz", + "integrity": "sha512-GBzP8zOc7CGWyFQS6dv1lQz8VVpz5C2yRszbXufwG/9zhStTIH50EtD87NmWbTMwXDvZLNg8GIpb1UFdH93JCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nuxt/devtools": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@nuxt/devtools/-/devtools-2.6.5.tgz", + "integrity": "sha512-Xh9XF1SzCTL5Zj6EULqsN2UjiNj4zWuUpS69rGAy5C55UTaj+Wn46IkDc6Q0+EKkGI279zlG6SzPRFawqPPUEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/devtools-kit": "2.6.5", + "@nuxt/devtools-wizard": "2.6.5", + "@nuxt/kit": "^3.19.2", + "@vue/devtools-core": "^7.7.7", + "@vue/devtools-kit": "^7.7.7", + "birpc": "^2.5.0", + "consola": "^3.4.2", + "destr": "^2.0.5", + "error-stack-parser-es": "^1.0.5", + "execa": "^8.0.1", + "fast-npm-meta": "^0.4.6", + "get-port-please": "^3.2.0", + "hookable": "^5.5.3", + "image-meta": "^0.2.1", + "is-installed-globally": "^1.0.0", + "launch-editor": "^2.11.1", + "local-pkg": "^1.1.2", + "magicast": "^0.3.5", + "nypm": "^0.6.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.3.0", + "semver": "^7.7.2", + "simple-git": "^3.28.0", + "sirv": "^3.0.2", + "structured-clone-es": "^1.0.0", + "tinyglobby": "^0.2.15", + "vite-plugin-inspect": "^11.3.3", + "vite-plugin-vue-tracer": "^1.0.0", + "which": "^5.0.0", + "ws": "^8.18.3" + }, + "bin": { + "devtools": "cli.mjs" + }, + "peerDependencies": { + "vite": ">=6.0" + } + }, + "node_modules/@nuxt/devtools-kit": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@nuxt/devtools-kit/-/devtools-kit-2.6.5.tgz", + "integrity": "sha512-t+NxoENyzJ8KZDrnbVYv3FJI5VXqSi6X4w6ZsuIIh0aKABu6+6k9nR/LoEhrM0oekn/2LDhA0NmsRZyzCXt2xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/kit": "^3.19.2", + "execa": "^8.0.1" + }, + "peerDependencies": { + "vite": ">=6.0" + } + }, + "node_modules/@nuxt/devtools-wizard": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@nuxt/devtools-wizard/-/devtools-wizard-2.6.5.tgz", + "integrity": "sha512-nYYGxT4lmQDvfHL6qolNWLu0QTavsdN/98F57falPuvdgs5ev1NuYsC12hXun+5ENcnigEcoM9Ij92qopBgqmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.4.2", + "diff": "^8.0.2", + "execa": "^8.0.1", + "magicast": "^0.3.5", + "pathe": "^2.0.3", + "pkg-types": "^2.3.0", + "prompts": "^2.4.2", + "semver": "^7.7.2" + }, + "bin": { + "devtools-wizard": "cli.mjs" + } + }, + "node_modules/@nuxt/devtools/node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nuxt/kit": { + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.19.2.tgz", + "integrity": "sha512-+QiqO0WcIxsKLUqXdVn3m4rzTRm2fO9MZgd330utCAaagGmHsgiMJp67kE14boJEPutnikfz3qOmrzBnDIHUUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "c12": "^3.2.0", + "consola": "^3.4.2", + "defu": "^6.1.4", + "destr": "^2.0.5", + "errx": "^0.1.0", + "exsolve": "^1.0.7", + "ignore": "^7.0.5", + "jiti": "^2.5.1", + "klona": "^2.0.6", + "knitwork": "^1.2.0", + "mlly": "^1.8.0", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "pkg-types": "^2.3.0", + "rc9": "^2.1.2", + "scule": "^1.3.0", + "semver": "^7.7.2", + "std-env": "^3.9.0", + "tinyglobby": "^0.2.15", + "ufo": "^1.6.1", + "unctx": "^2.4.1", + "unimport": "^5.2.0", + "untyped": "^2.0.0" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/@nuxt/telemetry": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/@nuxt/telemetry/-/telemetry-2.6.6.tgz", + "integrity": "sha512-Zh4HJLjzvm3Cq9w6sfzIFyH9ozK5ePYVfCUzzUQNiZojFsI2k1QkSBrVI9BGc6ArKXj/O6rkI6w7qQ+ouL8Cag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/kit": "^3.15.4", + "citty": "^0.1.6", + "consola": "^3.4.2", + "destr": "^2.0.3", + "dotenv": "^16.4.7", + "git-url-parse": "^16.0.1", + "is-docker": "^3.0.0", + "ofetch": "^1.4.1", + "package-manager-detector": "^1.1.0", + "pathe": "^2.0.3", + "rc9": "^2.1.2", + "std-env": "^3.8.1" + }, + "bin": { + "nuxt-telemetry": "bin/nuxt-telemetry.mjs" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/@nuxt/vite-builder": { + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/@nuxt/vite-builder/-/vite-builder-3.19.2.tgz", + "integrity": "sha512-SESdHAKWy63RKG3uqrBEJvTbfkmEsiggmDEqjEwhBP2fe0E6bGTmLpX/Eh4AuOLbZuZOmir984OHFiM/Q/MLhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/kit": "3.19.2", + "@rollup/plugin-replace": "^6.0.2", + "@vitejs/plugin-vue": "^6.0.1", + "@vitejs/plugin-vue-jsx": "^5.1.1", + "autoprefixer": "^10.4.21", + "consola": "^3.4.2", + "cssnano": "^7.1.1", + "defu": "^6.1.4", + "esbuild": "^0.25.9", + "escape-string-regexp": "^5.0.0", + "exsolve": "^1.0.7", + "externality": "^1.0.2", + "get-port-please": "^3.2.0", + "h3": "^1.15.4", + "jiti": "^2.5.1", + "knitwork": "^1.2.0", + "magic-string": "^0.30.19", + "mlly": "^1.8.0", + "mocked-exports": "^0.1.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.0.0", + "pkg-types": "^2.3.0", + "postcss": "^8.5.6", + "rollup-plugin-visualizer": "^6.0.3", + "std-env": "^3.9.0", + "ufo": "^1.6.1", + "unenv": "^2.0.0-rc.21", + "vite": "^7.1.5", + "vite-node": "^3.2.4", + "vite-plugin-checker": "^0.10.3", + "vue-bundle-renderer": "^2.1.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vue": "^3.3.4" + } + }, + "node_modules/@nuxtjs/vuetify": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/@nuxtjs/vuetify/-/vuetify-1.12.3.tgz", + "integrity": "sha512-6uVL3cfESMB00eVjJTNkyU4jvuPTGPn1yteo7lQTH6v+fxHcPaOgvzVYHIKSHIz1DecuOiB5c9b+YjsRP5+C8A==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2", + "sass": "~1.32.13", + "sass-loader": "^10.2.0", + "vuetify": "^2.6", + "vuetify-loader": "^1.7.3" + } + }, + "node_modules/@nuxtjs/vuetify/node_modules/@vue/compiler-sfc": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz", + "integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==", + "peer": true, + "dependencies": { + "@babel/parser": "^7.23.5", + "postcss": "^8.4.14", + "source-map": "^0.6.1" + }, + "optionalDependencies": { + "prettier": "^1.18.2 || ^2.0.0" + } + }, + "node_modules/@nuxtjs/vuetify/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@nuxtjs/vuetify/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@nuxtjs/vuetify/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/@nuxtjs/vuetify/node_modules/sass": { + "version": "1.32.13", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.32.13.tgz", + "integrity": "sha512-dEgI9nShraqP7cXQH+lEXVf73WOPCse0QlFzSD8k+1TcOxCMwVXfQlr0jtoluZysQOyJGnfr21dLvYKDJq8HkA==", + "license": "MIT", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/@nuxtjs/vuetify/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@nuxtjs/vuetify/node_modules/vue": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz", + "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==", + "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-sfc": "2.7.16", + "csstype": "^3.1.0" + } + }, + "node_modules/@nuxtjs/vuetify/node_modules/vuetify": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.6.13.tgz", + "integrity": "sha512-HhJi52IzhfrmWFwcYFUiA1GRIzz9smbR06Lj61Ml5HgD5PBcMiDywUnNPVid1VsXO4qWpBU6kO3O89uTxH1yzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/johnleider" + }, + "peerDependencies": { + "vue": "^2.6.4" + } + }, + "node_modules/@nuxtjs/vuetify/node_modules/vuetify-loader": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/vuetify-loader/-/vuetify-loader-1.9.2.tgz", + "integrity": "sha512-8PP2w7aAs/rjA+Izec6qY7sHVb75MNrGQrDOTZJ5IEnvl+NiFhVpU2iWdRDZ3eMS842cWxSWStvkr+KJJKy+Iw==", + "license": "MIT", + "dependencies": { + "acorn": "^8.4.1", + "acorn-walk": "^8.2.0", + "decache": "^4.6.0", + "file-loader": "^6.2.0", + "loader-utils": "^2.0.0" + }, + "peerDependencies": { + "gm": "^1.23.0", + "pug": "^2.0.0 || ^3.0.0", + "sharp": "*", + "vue": "^2.7.2", + "vuetify": "^1.3.0 || ^2.0.0", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "gm": { + "optional": true + }, + "pug": { + "optional": true + }, + "sharp": { + "optional": true + } + } + }, + "node_modules/@oxc-minify/binding-android-arm64": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-android-arm64/-/binding-android-arm64-0.87.0.tgz", + "integrity": "sha512-ZbJmAfXvNAamOSnXId3BiM3DiuzlD1isqKjtmRFb/hpvChHHA23FSPrFcO16w+ugZKg33sZ93FinFkKtlC4hww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-minify/binding-darwin-arm64": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-darwin-arm64/-/binding-darwin-arm64-0.87.0.tgz", + "integrity": "sha512-ewmNsTY8YbjWOI8+EOWKTVATOYvG4Qq4zQHH5VFBeqhQPVusY1ORD6Ei+BijVKrnlbpjibLlkTl8IWqXCGK89A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-minify/binding-darwin-x64": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-darwin-x64/-/binding-darwin-x64-0.87.0.tgz", + "integrity": "sha512-qDH4w4EYttSC3Cs2VCh+CiMYKrcL2SNmnguBZXoUXe/RNk3csM+RhgcwdpX687xGvOhTFhH5PCIA84qh3ZpIbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-minify/binding-freebsd-x64": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-freebsd-x64/-/binding-freebsd-x64-0.87.0.tgz", + "integrity": "sha512-5kxjHlSev2A09rDeITk+LMHxSrU3Iu8pUb0Zp4m+ul8FKlB9FrvFkAYwbctin6g47O98s3Win7Ewhy0w8JaiUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-minify/binding-linux-arm-gnueabihf": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.87.0.tgz", + "integrity": "sha512-NjbGXnNaAl5EgyonaDg2cPyH2pTf5a/+AP/5SRCJ0KetpXV22ZSUCvcy04Yt4QqjMcDs+WnJaGVxwx15Ofr6Gw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-minify/binding-linux-arm-musleabihf": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.87.0.tgz", + "integrity": "sha512-llAjfCA0iV2LMMl+LTR3JhqAc2iQmj+DTKd0VWOrbNOuNczeE9D5kJFkqYplD73LrkuqxrX9oDeUjjeLdVBPXw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-minify/binding-linux-arm64-gnu": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.87.0.tgz", + "integrity": "sha512-tf2Shom09AaSmu7U1hYYcEFF/cd+20HtmQ8eyGsRkqD5bqUj6lDu8TNSU9FWZ9tcZ83NzyFMwXZWHyeeIIbpxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-minify/binding-linux-arm64-musl": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.87.0.tgz", + "integrity": "sha512-pgWeYfSprtpnJVea9Q5eI6Eo80lDGlMw2JdcSMXmShtBjEhBl6bvDNHlV+6kNfh7iT65y/uC6FR8utFrRghu8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-minify/binding-linux-riscv64-gnu": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.87.0.tgz", + "integrity": "sha512-O1QPczlT+lqNZVeKOdFxxL+s1RIlnixaJYFLrcqDcRyn82MGKLz7sAenBTFRQoIfLnSxtMGL6dqHOefYkQx7Cg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-minify/binding-linux-s390x-gnu": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.87.0.tgz", + "integrity": "sha512-tcwt3ZUWOKfNLXN2edxFVHMlIuPvbuyMaKmRopgljSCfFcNHWhfTNlxlvmECRNhuQ91EcGwte6F1dwoeMCNd7A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-minify/binding-linux-x64-gnu": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.87.0.tgz", + "integrity": "sha512-Xf4AXF14KXUzSnfgTcFLFSM0TykJhFw14+xwNvlAb6WdqXAKlMrz9joIAezc8dkW1NNscCVTsqBUPJ4RhvCM1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-minify/binding-linux-x64-musl": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-x64-musl/-/binding-linux-x64-musl-0.87.0.tgz", + "integrity": "sha512-LIqvpx9UihEW4n9QbEljDnfUdAWqhr6dRqmzSFwVAeLZRUECluLCDdsdwemrC/aZkvnisA4w0LFcFr3HmeTLJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-minify/binding-wasm32-wasi": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-wasm32-wasi/-/binding-wasm32-wasi-0.87.0.tgz", + "integrity": "sha512-h0xluvc+YryfH5G5dndjGHuA/D4Kp85EkPMxqoOjNudOKDCtdobEaC9horhCqnOOQ0lgn+PGFl3w8u4ToOuRrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.0.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-minify/binding-win32-arm64-msvc": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.87.0.tgz", + "integrity": "sha512-fgxSx+TUc7e2rNtRAMnhHrjqh1e8p/JKmWxRZXtkILveMr/TOHGiDis7U3JJbwycmTZ+HSsJ/PNFQl+tKzmDxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-minify/binding-win32-x64-msvc": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-minify/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.87.0.tgz", + "integrity": "sha512-K6TTrlitEJgD0FGIW2r0t3CIJNqBkzHT97h49gZLS24ey2UG1zKt27iSHkpXMJYDiG97ZD2yv3pSph1ctMlFXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.87.0.tgz", + "integrity": "sha512-3APxTyYaAjpW5zifjzfsPgoIa4YHwA5GBjtgLRQpGVXCykXBIEbUTokoAs411ZuOwS3sdTVXBTGAdziXRd8rUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.87.0.tgz", + "integrity": "sha512-99e8E76M+k3Gtwvs5EU3VTs2hQkJmvnrl/eu7HkBUc9jLFHA4nVjYSgukMuqahWe270udUYEPRfcWKmoE1Nukg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.87.0.tgz", + "integrity": "sha512-2rRo6Dz560/4ot5Q0KPUTEunEObkP8mDC9mMiH0RJk1FiOb9c+xpPbkYoUHNKuVMm8uIoiBCxIAbPtBhs9QaXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.87.0.tgz", + "integrity": "sha512-uR+WZAvWkFQPVoeqXgQFr7iy+3hEI295qTbQ4ujmklgM5eTX3YgMFoIV00Stloxfd1irSDDSaK7ySnnzF6mRJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.87.0.tgz", + "integrity": "sha512-Emm1NpVGKbwzQOIZJI8ZuZu0z8FAd5xscqdS6qpDFpDdEMxk6ab7o3nM8V09RhNCORAzeUlk4TBHQ2Crzjd50A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.87.0.tgz", + "integrity": "sha512-1PPCxRZSJXzQaqc8y+wH7EqPgSfQ/JU3pK6WTN/1SUe/8paNVSKKqk175a8BbRVxGUtPnwEG89pi+xfPTSE7GA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.87.0.tgz", + "integrity": "sha512-fcnnsfcyLamJOMVKq+BQ8dasb8gRnZtNpCUfZhaEFAdXQ7J2RmZreFzlygcn80iti0V7c5LejcjHbF4IdK3GAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.87.0.tgz", + "integrity": "sha512-tBPkSPgRSSbmrje8CUovISi/Hj/tWjZJ3n/qnrjx2B+u86hWtwLsngtPDQa5d4seSyDaHSx6tNEUcH7+g5Ee0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.87.0.tgz", + "integrity": "sha512-z4UKGM4wv2wEAQAlx2pBq6+pDJw5J/5oDEXqW6yBSLbWLjLDo4oagmRSE3+giOWteUa+0FVJ+ypq4iYxBkYSWg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.87.0.tgz", + "integrity": "sha512-6W1ENe/nZtr2TBnrEzmdGEraEAdZOiH3YoUNNeQWuqwLkmpoHTJJdclieToPe/l2IKJ4WL3FsSLSGHE8yt/OEg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.87.0.tgz", + "integrity": "sha512-s3kB/Ii3X3IOZ27Iu7wx2zYkIcDO22Emu32SNC6kkUSy09dPBc1yaW14TnAkPMe/rvtuzR512JPWj3iGpl+Dng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.87.0.tgz", + "integrity": "sha512-3+M9hfrZSDi4+Uy4Ll3rtOuVG3IHDQlj027jgtmAAHJK1eqp4CQfC7rrwE+LFUqUwX+KD2GwlxR+eHyyEf5Gbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.87.0.tgz", + "integrity": "sha512-2jgeEeOa4GbQQg2Et/gFTgs5wKS/+CxIg+CN2mMOJ4EqbmvUVeGiumO01oFOWTYnJy1oONwIocBzrnMuvOcItA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.0.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.87.0.tgz", + "integrity": "sha512-KZp9poaBaVvuFM0TrsHCDOjPQK5eMDXblz21boMhKHGW5/bOlkMlg3CYn5j0f67FkK68NSdNKREMxmibBeXllQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.87.0.tgz", + "integrity": "sha512-86uisngtp/8XdcerIKxMyJTqgDSTJatkfpylpUH0d96W8Bb9E+bVvM2fIIhLWB0Eb03PeY2BdIT7DNIln9TnHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.87.0.tgz", + "integrity": "sha512-ipZFWVGE9fADBVXXWJWY/cxpysc41Gt5upKDeb32F6WMgFyO7XETUMVq8UuREKCih+Km5E6p2VhEvf6Fuhey6g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxc-transform/binding-android-arm64": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-android-arm64/-/binding-android-arm64-0.87.0.tgz", + "integrity": "sha512-B7W6J8T9cS054LUGLfYkYz8bz5+t+4yPftZ67Bn6MJ03okMLnbbEfm1bID1tqcP5tJwMurTILVy/dQfDYDcMgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-transform/binding-darwin-arm64": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-darwin-arm64/-/binding-darwin-arm64-0.87.0.tgz", + "integrity": "sha512-HImW3xOPx7FHKqfC5WfE82onhRfnWQUiB7R+JgYrk+7NR404h3zANSPzu3V/W9lbDxlmHTcqoD2LKbNC5j0TQA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-transform/binding-darwin-x64": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-darwin-x64/-/binding-darwin-x64-0.87.0.tgz", + "integrity": "sha512-MDbgugi6mvuPTfS78E2jyozm7493Kuqmpc5r406CsUdEsXlnsF+xvmKlrW9ZIkisO74dD+HWouSiDtNyPQHjlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-transform/binding-freebsd-x64": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-freebsd-x64/-/binding-freebsd-x64-0.87.0.tgz", + "integrity": "sha512-N0M5D/4haJw7BMn2WZ3CWz0WkdLyoK1+3KxOyCv2CPedMCxx6eQay2AtJxSzj9tjVU1+ukbSb2fDO24JIJGsVA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-transform/binding-linux-arm-gnueabihf": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.87.0.tgz", + "integrity": "sha512-PubObCNOUOzm1S+P0yn7S+/6xRLbSPMqhgrb73L3p+J1Z20fv/FYVg0kFd36Yho24TSC/byOkebEZWAtxCasWw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-transform/binding-linux-arm-musleabihf": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.87.0.tgz", + "integrity": "sha512-Nk2d/FS7sMCmCl99vHojzigakjDPamkjOXs2i+H71o/NqytS0pk3M+tXat8M3IGpeLJIEszA5Mv+dcq731nlYA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-transform/binding-linux-arm64-gnu": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.87.0.tgz", + "integrity": "sha512-BxFkIcso2V1+FCDoU+KctxvJzSQVSnEZ5EEQ8O3Up9EoFVQRnZ8ktXvqYj2Oqvc4IYPskLPsKUgc9gdK8wGhUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-transform/binding-linux-arm64-musl": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.87.0.tgz", + "integrity": "sha512-MZ1/TNaebhXK73j1UDfwyBFnAy0tT3n6otOkhlt1vlJwqboUS/D7E/XrCZmAuHIfVPxAXRPovkl7kfxLB43SKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-transform/binding-linux-riscv64-gnu": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.87.0.tgz", + "integrity": "sha512-JCWE6n4Hicu0FVbvmLdH/dS8V6JykOUsbrbDYm6JwFlHr4eFTTlS2B+mh5KPOxcdeOlv/D/XRnvMJ6WGYs25EA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-transform/binding-linux-s390x-gnu": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.87.0.tgz", + "integrity": "sha512-n2NTgM+3PqFagJV9UXRDNOmYesF+TO9SF9FeHqwVmW893ayef9KK+vfWAAhvOYHXYaKWT5XoHd87ODD7nruyhw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-transform/binding-linux-x64-gnu": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.87.0.tgz", + "integrity": "sha512-ZOKW3wx0bW2O7jGdOzr8DyLZqX2C36sXvJdsHj3IueZZ//d/NjLZqEiUKz+q0JlERHtCVKShQ5PLaCx7NpuqNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-transform/binding-linux-x64-musl": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-x64-musl/-/binding-linux-x64-musl-0.87.0.tgz", + "integrity": "sha512-eIspx/JqkVMPK1CAYEOo2J8o49s4ZTf+32MSMUknIN2ZS1fvRmWS0D/xFFaLP/9UGhdrXRIPbn/iSYEA8JnV/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-transform/binding-wasm32-wasi": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-wasm32-wasi/-/binding-wasm32-wasi-0.87.0.tgz", + "integrity": "sha512-4uRjJQnt/+kmJUIC6Iwzn+MqqZhLP1zInPtDwgL37KI4VuUewUQWoL+sggMssMEgm7ZJwOPoZ6piuSWwMgOqgQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.0.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-transform/binding-win32-arm64-msvc": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.87.0.tgz", + "integrity": "sha512-l/qSi4/N5W1yXKU9+1gWGo0tBoRpp4zvHYrpsbq3zbefPL4VYdA0gKF7O10/ZQVkYylzxiVh2zpYO34/FbZdIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-transform/binding-win32-x64-msvc": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.87.0.tgz", + "integrity": "sha512-jG/MhMjfSdyj5KyhnwNWr4mnAlAsz+gNUYpjQ+UXWsfsoB3f8HqbsTkG02RBtNa/IuVQYvYYVf1eIimNN3gBEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-wasm": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-wasm/-/watcher-wasm-2.5.1.tgz", + "integrity": "sha512-RJxlQQLkaMMIuWRozy+z2vEqbaQlCuaCgVZIUCzQLYggY22LZbP5Y1+ia+FD724Ids9e+XIyOLXLrLgQSHIthw==", + "bundleDependencies": [ + "napi-wasm" + ], + "dev": true, + "license": "MIT", + "dependencies": { + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "napi-wasm": "^1.1.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-wasm/node_modules/napi-wasm": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@poppinss/colors": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.5.tgz", + "integrity": "sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/colors/node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.4.tgz", + "integrity": "sha512-iG0TIdqv8xJ3Lt9O8DrPRxw1MRLjNpoqiSGU03P/wNLP/s0ra0udPJ1J2Tx5M0J3H/cVyEgpbn8xUKRY9j59kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.2.tgz", + "integrity": "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz", + "integrity": "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-alias": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-5.1.1.tgz", + "integrity": "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.6", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.6.tgz", + "integrity": "sha512-XSQB1K7FUU5QP+3lOQmVCE3I0FcbbNvmNT4VJSj93iUjayaARrTQeoRdiYQoftAJBLrR9t2agwAd3ekaTgHNlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-inject": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz", + "integrity": "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-inject/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz", + "integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.2.tgz", + "integrity": "sha512-7QaYCf8bqF04dOy7w/eHmJeNExxTYwvKAmlSAH/EaWWUzbT0h5sbF6bktFoX/0F/0qwng5/dWFMyf3gzaM8DsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.1.tgz", + "integrity": "sha512-sifE8uDpDvortUdi3xFevQ9WN5L3orrglg7iO/DhIpSVCwJOxBs9k9JzCC76KEZkLY4UkHWj+KESdFhlsNmDLw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.1.tgz", + "integrity": "sha512-s83W/rRAPshsyzH9cS0CPKZVLlo2GGRt/1BocbR64DIyr2tMN1f2OZEjbFUnkAA2ewfbd+9waSYS0vbrlsG3qg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.1.tgz", + "integrity": "sha512-lJkbZBREVUY9Vdw6DrzCysWv9Trcl7SyNxPRQMqvt6V/xmQC140aOcSkyWzwQ9t+s3ojvvWYZMpSazAbSTNfSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.1.tgz", + "integrity": "sha512-cw852iGDmvuXeOz2lwpocEL9wkHg3TBZRdAbwmra/YJ5KVxaj7nDdYJ9P0OAVxsbsKa0hFML+dwRHA02kB8Q+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.1.tgz", + "integrity": "sha512-nLezpaKL1jY63BunCbeA7B7B/5i4DQifNRBfzZ0+p3BxRejeKdzP7T3rfD5YpNy3+RysFy8Zw3EAnvXyrbZzqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.1.tgz", + "integrity": "sha512-USdXZmfo+t4DoUC02UotEf7e6ADsaQ1pvOtOZV2iT2wEmB6y7iMJA0MsIZTbp27enq9v+YK43s3ztYPVy0T2bA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.1.tgz", + "integrity": "sha512-n3YunK17pY3BuZhLNTcRCT83JkFRfBKnG4R2vROUZvxLJlYkIQXfDGQRVZ7ZZBp1INxXm4fzT4jrd6Tm5DMZ7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.1.tgz", + "integrity": "sha512-45geWgFvA+SKw49tRkHI7xBizBZc6bismWIg+zqwK1OZN0hqMXe39BExVu45o768KDoM7XGoZ1pDE9opiHKKag==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.1.tgz", + "integrity": "sha512-7m2ybyIOd5j/U43JSfMblwiZG69yAfuvg6TXhHvOtoQMjw6Or48FmgUxyAZ4ZzH7isxfMyr8M26m0pBkoAIEdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.1.tgz", + "integrity": "sha512-qnmMzRpkKG1T1EzKVtA/8Q0YAYalRN+h+WzWcbyD0SqjVwxmqrPj/TuuH30TwUp6X2UaUhfWSHccMgF+T6jDpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.1.tgz", + "integrity": "sha512-5Fc7jWzggy8RXJTew+8FoUXwpvJIuwOcYEMSJxs/9MB+oG/C4NRM23Xg+vW173sQz0H6RSViMmoKJih/hVQQow==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.1.tgz", + "integrity": "sha512-DxnsniAn/iv23PtQhOU0l+cXAG3IvWkzEOc9t4THzWJs/NKpF955GnbYKo6PwqwlcbxO/ARn4B8IMg4ghW+DOw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.1.tgz", + "integrity": "sha512-xAlxc3PeGHNpLmisSs8UpFm/A8aPOVeoHhWePEH0rDVFCC4uwWx4W1ecq/oYT2gjkRtVBxD1GjjNYJQrN9fX4A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.1.tgz", + "integrity": "sha512-b5xbekmUtAkPY3TqrYMvbAltNNmpMApdMDxjYiaUQ8k1ep0iS/900CJEZq/RPd5gXF59Lp+me1wXbkW1xpxw4g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.1.tgz", + "integrity": "sha512-CcNQx6CuvJH/SMt3dElyqrCK7BCCAOQtdobJIVhJ7AaA5nrE0RkNHTVzDyXkYqkgoMjuF2p0tEchX7YuOeal4w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.1.tgz", + "integrity": "sha512-xsKzVShwurM4JjGyMo/n4lb13mzpfDmg0yWiMlO65XSkhIpWnGnE4z66y9leVALb3M7sWiNluCKUv2ZZ0DWy1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.1.tgz", + "integrity": "sha512-AtzCeCyU6wYbJq7akOX3oZmc1pcY6yNYYC+HbjAcnjB63hXc22AX6nWtoU9TOJw3EQRxCLIubwGmnSrk66khpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.1.tgz", + "integrity": "sha512-pZb5K1hqS6MmdSgNUfWIzemPNNwmg5n7HhZHSyClwGd/IoQCiTjUGs09O/lxOZLHlltqUyVl0Y/4dcd8j90FEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.1.tgz", + "integrity": "sha512-A6hkNBmS3yahy06sFIouOjC5MO/ciPSBxdbWdGIk7ue3lhR1wJ9mJ27kZFK/N8ZOLwO1YdymYhhfI3gGHHpliA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.1.tgz", + "integrity": "sha512-HRNyKIYDpuC7FIVJ8kH1RFGoEp4beASrjKksx3f2Oa82pLxNVhBIM1gC7WEd7z9djZ0OW6o9qhXFo7gAU4QCWw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.1.tgz", + "integrity": "sha512-rkpnc4BKw8QoP9yynwLJqjVgmkko8yjqEHHYlUPv/xznRb3mQ7iN7fpc5fOqCFtYCeEyilBAun5a4wKLLKYX2g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.1.tgz", + "integrity": "sha512-ZzNEDNx/4sWP94UNAc6OfVNJFM2G4vz6IcIhBJv8BYyLeGNQldV5Dn22+i8Y7yn4a7unFjdAX/1nwNBfc7tUcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.0.tgz", + "integrity": "sha512-7F/yz2IphV39hiS2zB4QYVkivrptHHh0K8qJJd9HhuWSdvf8AN7NpebW3CcDZDBQsUPMoDKWsY2WWgW7bqOcfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.7.tgz", + "integrity": "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.12.0" + } + }, + "node_modules/@types/parse-path": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz", + "integrity": "sha512-LriObC2+KYZD3FzCrgWGv/qufdUy4eXrxcLgQMfYXgPbLIecKIsVBaQgUPmxSSLcjmYbDTQbMgr6qr6l/eb7Bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@unhead/vue": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.0.17.tgz", + "integrity": "sha512-jzmGZYeMAhETV6qfetmLbZzUjjx1TjdNvFSobeFZb73D7dwD9wl/nOAx36qq+TvjZsLJdF5PQWToz2oDGAUqCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookable": "^5.5.3", + "unhead": "2.0.17" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + }, + "peerDependencies": { + "vue": ">=3.5.18" + } + }, + "node_modules/@vercel/nft": { + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.30.1.tgz", + "integrity": "sha512-2mgJZv4AYBFkD/nJ4QmiX5Ymxi+AisPLPcS/KPXVqniyQNqKXX+wjieAbDXQP3HcogfEbpHoRMs49Cd4pfkk8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^2.0.0", + "@rollup/pluginutils": "^5.1.3", + "acorn": "^8.6.0", + "acorn-import-attributes": "^1.9.5", + "async-sema": "^3.1.1", + "bindings": "^1.4.0", + "estree-walker": "2.0.2", + "glob": "^10.4.5", + "graceful-fs": "^4.2.9", + "node-gyp-build": "^4.2.2", + "picomatch": "^4.0.2", + "resolve-from": "^5.0.0" + }, + "bin": { + "nft": "out/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vercel/nft/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz", + "integrity": "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.29" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitejs/plugin-vue-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-5.1.1.tgz", + "integrity": "sha512-uQkfxzlF8SGHJJVH966lFTdjM/lGcwJGzwAHpVqAPDD/QcsqoUGa+q31ox1BrUfi+FLP2ChVp7uLXE3DkHyDdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.3", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.0", + "@rolldown/pluginutils": "^1.0.0-beta.34", + "@vue/babel-plugin-jsx": "^1.5.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", + "vue": "^3.0.0" + } + }, + "node_modules/@vitejs/plugin-vue-jsx/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.39", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.39.tgz", + "integrity": "sha512-GkTtNCV8ObWbq3LrJStPBv9jkRPct8WlwotVjx3aU0RwfH3LyheixWK9Zhaj22C4EQj/TJxYyetoX+uOn/MWKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/language-core": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz", + "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.23" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz", + "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz", + "integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue-macros/common": { + "version": "3.0.0-beta.16", + "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.0.0-beta.16.tgz", + "integrity": "sha512-8O2gWxWFiaoNkk7PGi0+p7NPGe/f8xJ3/INUufvje/RZOs7sJvlI1jnR4lydtRFa/mU0ylMXUXXjSK0fHDEYTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-sfc": "^3.5.17", + "ast-kit": "^2.1.1", + "local-pkg": "^1.1.1", + "magic-string-ast": "^1.0.0", + "unplugin-utils": "^0.2.4" + }, + "engines": { + "node": ">=20.18.0" + }, + "funding": { + "url": "https://github.com/sponsors/vue-macros" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.2.25" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", + "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz", + "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", + "@vue/babel-helper-vue-transform-on": "1.5.0", + "@vue/babel-plugin-resolve-type": "1.5.0", + "@vue/shared": "^3.5.18" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz", + "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/parser": "^7.28.0", + "@vue/compiler-sfc": "^3.5.18" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.21.tgz", + "integrity": "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/shared": "3.5.21", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.21.tgz", + "integrity": "sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.21", + "@vue/shared": "3.5.21" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz", + "integrity": "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/compiler-core": "3.5.21", + "@vue/compiler-dom": "3.5.21", + "@vue/compiler-ssr": "3.5.21", + "@vue/shared": "3.5.21", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.18", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.21.tgz", + "integrity": "sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.21", + "@vue/shared": "3.5.21" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/devtools-core": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.7.7.tgz", + "integrity": "sha512-9z9TLbfC+AjAi1PQyWX+OErjIaJmdFlbDHcD+cAMYKY6Bh5VlsAtCeGyRMrXwIlMEQPukvnWt3gZBLwTAIMKzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.7", + "@vue/devtools-shared": "^7.7.7", + "mitt": "^3.0.1", + "nanoid": "^5.1.0", + "pathe": "^2.0.3", + "vite-hot-client": "^2.0.4" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz", + "integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.7", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-kit/node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz", + "integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/language-core": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.0.8.tgz", + "integrity": "sha512-eYs6PF7bxoPYvek9qxceo1BCwFbJZYqJll+WaYC8o8ec60exqj+n+QRGGiJHSeUfYp0hDxARbMdxMq/fbPgU5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^2.0.5", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.21.tgz", + "integrity": "sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.21" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.21.tgz", + "integrity": "sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.21", + "@vue/shared": "3.5.21" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.21.tgz", + "integrity": "sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.21", + "@vue/runtime-core": "3.5.21", + "@vue/shared": "3.5.21", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.21.tgz", + "integrity": "sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.21", + "@vue/shared": "3.5.21" + }, + "peerDependencies": { + "vue": "3.5.21" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.21.tgz", + "integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/alien-signals": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-2.0.7.tgz", + "integrity": "sha512-wE7y3jmYeb0+h6mr5BOovuqhFv22O/MV9j5p0ndJsa7z1zJNPGQ4ph5pQk/kTTCWRC3xsA4SmtwmkzQO+7NCNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.1.0.tgz", + "integrity": "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ast-kit": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.1.2.tgz", + "integrity": "sha512-cl76xfBQM6pztbrFWRnxbrDm9EOqDr1BF6+qQnnDZG2Co2LjyUktkN9GTJfBAfdae+DbT2nJf2nCGAdDDN7W2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.18.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/ast-walker-scope": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.8.2.tgz", + "integrity": "sha512-3pYeLyDZ6nJew9QeBhS4Nly02269Dkdk32+zdbbKmL6n4ZuaGorwwA+xx12xgOciA8BF1w9x+dlH7oUkFTW91w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "ast-kit": "^2.1.2" + }, + "engines": { + "node": ">=20.18.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-sema": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz", + "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "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.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "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/b4a": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.1.tgz", + "integrity": "sha512-ZovbrBV0g6JxK5cGUF1Suby1vLfKjv4RWi8IxoaO/Mon8BDD9I21RxjHFtgQ+kskJqLAVyQZly3uMBui+vhc8Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.7.0.tgz", + "integrity": "sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz", + "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/birpc": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.6.1.tgz", + "integrity": "sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", + "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "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.8.3", + "caniuse-lite": "^1.0.30001741", + "electron-to-chromium": "^1.5.218", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/c12": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.0.tgz", + "integrity": "sha512-K9ZkuyeJQeqLEyqldbYLG3wjqwpw4BVaAqvmxq3GYKK0b1A/yYQdIcJxkzAOWcNVWhJpRXAPfZFueekiY/L8Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^17.2.2", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.5.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.0.0", + "pkg-types": "^2.3.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", + "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==", + "engines": { + "node": "*" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001743", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", + "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", + "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/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/clipboardy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-4.0.0.tgz", + "integrity": "sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^8.0.1", + "is-wsl": "^3.1.0", + "is64bit": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/compatx": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/compatx/-/compatx-0.2.0.tgz", + "integrity": "sha512-6gLRNt4ygsi5NyMVhceOCFv14CIdDFN7fQjX1U4+47qVE/+kjPoXMK65KWK+dWxmFzMTuKazoQ9sch6pM0p5oA==", + "dev": true, + "license": "MIT" + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cookie-es": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz", + "integrity": "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/croner": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/croner/-/croner-9.1.0.tgz", + "integrity": "sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/css-declaration-sorter": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.0.tgz", + "integrity": "sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz", + "integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^7.0.9", + "lilconfig": "^3.1.3" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/cssnano-preset-default": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.9.tgz", + "integrity": "sha512-tCD6AAFgYBOVpMBX41KjbvRh9c2uUjLXRyV7KHSIrwHiq5Z9o0TFfUCoM3TwVrRsRteN3sVXGNvjVNxYzkpTsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^5.0.1", + "postcss-calc": "^10.1.1", + "postcss-colormin": "^7.0.4", + "postcss-convert-values": "^7.0.7", + "postcss-discard-comments": "^7.0.4", + "postcss-discard-duplicates": "^7.0.2", + "postcss-discard-empty": "^7.0.1", + "postcss-discard-overridden": "^7.0.1", + "postcss-merge-longhand": "^7.0.5", + "postcss-merge-rules": "^7.0.6", + "postcss-minify-font-values": "^7.0.1", + "postcss-minify-gradients": "^7.0.1", + "postcss-minify-params": "^7.0.4", + "postcss-minify-selectors": "^7.0.5", + "postcss-normalize-charset": "^7.0.1", + "postcss-normalize-display-values": "^7.0.1", + "postcss-normalize-positions": "^7.0.1", + "postcss-normalize-repeat-style": "^7.0.1", + "postcss-normalize-string": "^7.0.1", + "postcss-normalize-timing-functions": "^7.0.1", + "postcss-normalize-unicode": "^7.0.4", + "postcss-normalize-url": "^7.0.1", + "postcss-normalize-whitespace": "^7.0.1", + "postcss-ordered-values": "^7.0.2", + "postcss-reduce-initial": "^7.0.4", + "postcss-reduce-transforms": "^7.0.1", + "postcss-svgo": "^7.1.0", + "postcss-unique-selectors": "^7.0.4" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/cssnano-utils": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.1.tgz", + "integrity": "sha512-ZIP71eQgG9JwjVZsTPSqhc6GHgEr53uJ7tK5///VfyWj6Xp2DBmixWHqJgPno+PqATzn48pL42ww9x5SSGmhZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/db0": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/db0/-/db0-0.3.2.tgz", + "integrity": "sha512-xzWNQ6jk/+NtdfLyXEipbX55dmDSeteLFt/ayF+wZUU5bzKgmrDOxmInUTbyVRp46YwnJdkDA1KhB7WIXFofJw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@electric-sql/pglite": "*", + "@libsql/client": "*", + "better-sqlite3": "*", + "drizzle-orm": "*", + "mysql2": "*", + "sqlite3": "*" + }, + "peerDependenciesMeta": { + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "drizzle-orm": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decache": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/decache/-/decache-4.6.2.tgz", + "integrity": "sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==", + "license": "MIT", + "dependencies": { + "callsite": "^1.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/devalue": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.3.2.tgz", + "integrity": "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-prop": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", + "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.222", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz", + "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/errx": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/errx/-/errx-0.1.0.tgz", + "integrity": "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/externality": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/externality/-/externality-1.0.2.tgz", + "integrity": "sha512-LyExtJWKxtgVzmgtEHyQtLFpw1KFhQphF9nTG8TpAIVkiI/xQ3FJh75tRFLYl4hkn7BNIIdLJInuDAavX35pMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "enhanced-resolve": "^5.14.1", + "mlly": "^1.3.0", + "pathe": "^1.1.1", + "ufo": "^1.1.2" + } + }, + "node_modules/externality/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-npm-meta": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/fast-npm-meta/-/fast-npm-meta-0.4.6.tgz", + "integrity": "sha512-zbBBOAOlzxfrU4WSnbCHk/nR6Vf32lSEPxDEvNOR08Z5DSZ/A6qJu0rqrHVcexBTd1hc2gim998xnqF/R1PuEw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuse.js": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", + "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/git-up": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-8.1.1.tgz", + "integrity": "sha512-FDenSF3fVqBYSaJoYy1KSc2wosx0gCvKP+c+PRBht7cAaiCeQlBtfBDX9vgnNOHmdePlSFITVcn4pFfcgNvx3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-ssh": "^1.4.0", + "parse-url": "^9.2.0" + } + }, + "node_modules/git-url-parse": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-16.1.0.tgz", + "integrity": "sha512-cPLz4HuK86wClEW7iDdeAKcCVlWXmrLpb2L+G9goW0Z1dtpNS6BXXSOckUTlJT/LDQViE1QZKstNORzHsLnobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "git-up": "^8.1.0" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/gzip-size": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-7.0.0.tgz", + "integrity": "sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/h3": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.4.tgz", + "integrity": "sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.2", + "crossws": "^0.3.5", + "defu": "^6.1.4", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.2", + "radix3": "^1.1.2", + "ufo": "^1.6.1", + "uncrypto": "^0.1.3" + } + }, + "node_modules/h3/node_modules/cookie-es": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", + "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-shutdown": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/http-shutdown/-/http-shutdown-1.2.2.tgz", + "integrity": "sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/httpxy": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/httpxy/-/httpxy-0.1.7.tgz", + "integrity": "sha512-pXNx8gnANKAndgga5ahefxc++tJvNL87CXoRwxn1cJE2ZkWEojF3tNfQIEhZX/vfpt+wzeAzpUI4qkediX1MLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-meta": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/image-meta/-/image-meta-0.2.1.tgz", + "integrity": "sha512-K6acvFaelNxx8wc2VjbIzXKDVB0Khs0QT35U6NkGfTdCmjLNcO2945m7RFNR9/RPVFm48hq7QPzK8uGH18HCGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/immutable": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz", + "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/impound": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/impound/-/impound-1.0.0.tgz", + "integrity": "sha512-8lAJ+1Arw2sMaZ9HE2ZmL5zOcMnt18s6+7Xqgq2aUVy4P1nlzAyPtzCDxsk51KVFwHEEdc6OWvUyqwHwhRYaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "exsolve": "^1.0.5", + "mocked-exports": "^0.1.1", + "pathe": "^2.0.3", + "unplugin": "^2.3.2", + "unplugin-utils": "^0.2.4" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ioredis": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz", + "integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.3.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", + "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-ssh": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.1.tgz", + "integrity": "sha512-JNeu1wQsHjyHgn9NcWTaXq6zWSR6hqE0++zhfZlkFBbScNkyvxCdeV8sRkSBaeLKxmbpR21brail63ACNxJ0Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "protocols": "^2.0.1" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is64bit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is64bit/-/is64bit-2.0.0.tgz", + "integrity": "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "system-architecture": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", + "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT", + "peer": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/knitwork": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/knitwork/-/knitwork-1.2.0.tgz", + "integrity": "sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/launch-editor": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz", + "integrity": "sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss/node_modules/detect-libc": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz", + "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/listhen": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/listhen/-/listhen-1.9.0.tgz", + "integrity": "sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/watcher": "^2.4.1", + "@parcel/watcher-wasm": "^2.4.1", + "citty": "^0.1.6", + "clipboardy": "^4.0.0", + "consola": "^3.2.3", + "crossws": ">=0.2.0 <0.4.0", + "defu": "^6.1.4", + "get-port-please": "^3.1.2", + "h3": "^1.12.0", + "http-shutdown": "^1.2.2", + "jiti": "^2.1.2", + "mlly": "^1.7.1", + "node-forge": "^1.3.1", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "ufo": "^1.5.4", + "untun": "^0.1.3", + "uqr": "^0.1.2" + }, + "bin": { + "listen": "bin/listhen.mjs", + "listhen": "bin/listhen.mjs" + } + }, + "node_modules/listhen/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-regexp": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/magic-regexp/-/magic-regexp-0.10.0.tgz", + "integrity": "sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12", + "mlly": "^1.7.2", + "regexp-tree": "^0.1.27", + "type-level-regexp": "~0.1.17", + "ufo": "^1.5.4", + "unplugin": "^2.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magic-string-ast": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.2.tgz", + "integrity": "sha512-8ngQgLhcT0t3YBdn9CGkZqCYlvwW9pm7aWJwd7AxseVWf1RU8ZHCQvG1mt3N5vvUme+pXTcHB8G/7fE666U8Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17" + }, + "engines": { + "node": ">=20.18.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/mocked-exports": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/mocked-exports/-/mocked-exports-0.1.1.tgz", + "integrity": "sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/nanotar": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nanotar/-/nanotar-0.2.0.tgz", + "integrity": "sha512-9ca1h0Xjvo9bEkE4UOxgAzLV0jHKe6LMaxo37ND2DAhhAtd0j8pR1Wxz+/goMrZO8AEZTWCmyaOsFI/W5AdpCQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/nitropack": { + "version": "2.12.6", + "resolved": "https://registry.npmjs.org/nitropack/-/nitropack-2.12.6.tgz", + "integrity": "sha512-DEq31s0SP4/Z5DIoVBRo9DbWFPWwIoYD4cQMEz7eE+iJMiAP+1k9A3B9kcc6Ihc0jDJmfUcHYyh6h2XlynCx6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cloudflare/kv-asset-handler": "^0.4.0", + "@rollup/plugin-alias": "^5.1.1", + "@rollup/plugin-commonjs": "^28.0.6", + "@rollup/plugin-inject": "^5.0.5", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-replace": "^6.0.2", + "@rollup/plugin-terser": "^0.4.4", + "@vercel/nft": "^0.30.1", + "archiver": "^7.0.1", + "c12": "^3.2.0", + "chokidar": "^4.0.3", + "citty": "^0.1.6", + "compatx": "^0.2.0", + "confbox": "^0.2.2", + "consola": "^3.4.2", + "cookie-es": "^2.0.0", + "croner": "^9.1.0", + "crossws": "^0.3.5", + "db0": "^0.3.2", + "defu": "^6.1.4", + "destr": "^2.0.5", + "dot-prop": "^9.0.0", + "esbuild": "^0.25.9", + "escape-string-regexp": "^5.0.0", + "etag": "^1.8.1", + "exsolve": "^1.0.7", + "globby": "^14.1.0", + "gzip-size": "^7.0.0", + "h3": "^1.15.4", + "hookable": "^5.5.3", + "httpxy": "^0.1.7", + "ioredis": "^5.7.0", + "jiti": "^2.5.1", + "klona": "^2.0.6", + "knitwork": "^1.2.0", + "listhen": "^1.9.0", + "magic-string": "^0.30.19", + "magicast": "^0.3.5", + "mime": "^4.0.7", + "mlly": "^1.8.0", + "node-fetch-native": "^1.6.7", + "node-mock-http": "^1.0.3", + "ofetch": "^1.4.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.0.0", + "pkg-types": "^2.3.0", + "pretty-bytes": "^7.0.1", + "radix3": "^1.1.2", + "rollup": "^4.50.1", + "rollup-plugin-visualizer": "^6.0.3", + "scule": "^1.3.0", + "semver": "^7.7.2", + "serve-placeholder": "^2.0.2", + "serve-static": "^2.2.0", + "source-map": "^0.7.6", + "std-env": "^3.9.0", + "ufo": "^1.6.1", + "ultrahtml": "^1.6.0", + "uncrypto": "^0.1.3", + "unctx": "^2.4.1", + "unenv": "^2.0.0-rc.21", + "unimport": "^5.2.0", + "unplugin-utils": "^0.3.0", + "unstorage": "^1.17.1", + "untyped": "^2.0.0", + "unwasm": "^0.3.11", + "youch": "^4.1.0-beta.11", + "youch-core": "^0.3.3" + }, + "bin": { + "nitro": "dist/cli/index.mjs", + "nitropack": "dist/cli/index.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "xml2js": "^0.6.2" + }, + "peerDependenciesMeta": { + "xml2js": { + "optional": true + } + } + }, + "node_modules/nitropack/node_modules/unplugin-utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.0.tgz", + "integrity": "sha512-JLoggz+PvLVMJo+jZt97hdIIIZ2yTzGgft9e9q8iMrC4ewufl62ekeW7mixBghonn2gVb/ICjyvlmOCUBnJLQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-mock-http": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.3.tgz", + "integrity": "sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "license": "MIT" + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nuxt": { + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/nuxt/-/nuxt-3.19.2.tgz", + "integrity": "sha512-z4ouGRMOWqZ1xaZ+HdRBRVlZcKSoDBpRxQ30GJ2dllraZMC/gNpTGuY32H3xP5b4R29b8uYcK+B8LFQoRHpO8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/cli": "^3.28.0", + "@nuxt/devalue": "^2.0.2", + "@nuxt/devtools": "^2.6.3", + "@nuxt/kit": "3.19.2", + "@nuxt/schema": "3.19.2", + "@nuxt/telemetry": "^2.6.6", + "@nuxt/vite-builder": "3.19.2", + "@unhead/vue": "^2.0.14", + "@vue/shared": "^3.5.21", + "c12": "^3.2.0", + "chokidar": "^4.0.3", + "compatx": "^0.2.0", + "consola": "^3.4.2", + "cookie-es": "^2.0.0", + "defu": "^6.1.4", + "destr": "^2.0.5", + "devalue": "^5.3.2", + "errx": "^0.1.0", + "esbuild": "^0.25.9", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "exsolve": "^1.0.7", + "h3": "^1.15.4", + "hookable": "^5.5.3", + "ignore": "^7.0.5", + "impound": "^1.0.0", + "jiti": "^2.5.1", + "klona": "^2.0.6", + "knitwork": "^1.2.0", + "magic-string": "^0.30.19", + "mlly": "^1.8.0", + "mocked-exports": "^0.1.1", + "nanotar": "^0.2.0", + "nitropack": "^2.12.5", + "nypm": "^0.6.1", + "ofetch": "^1.4.1", + "ohash": "^2.0.11", + "on-change": "^5.0.1", + "oxc-minify": "^0.87.0", + "oxc-parser": "^0.87.0", + "oxc-transform": "^0.87.0", + "oxc-walker": "^0.5.2", + "pathe": "^2.0.3", + "perfect-debounce": "^2.0.0", + "pkg-types": "^2.3.0", + "radix3": "^1.1.2", + "scule": "^1.3.0", + "semver": "^7.7.2", + "std-env": "^3.9.0", + "tinyglobby": "^0.2.15", + "ufo": "^1.6.1", + "ultrahtml": "^1.6.0", + "uncrypto": "^0.1.3", + "unctx": "^2.4.1", + "unimport": "^5.2.0", + "unplugin": "^2.3.10", + "unplugin-vue-router": "^0.15.0", + "unstorage": "^1.17.1", + "untyped": "^2.0.0", + "vue": "^3.5.21", + "vue-bundle-renderer": "^2.1.2", + "vue-devtools-stub": "^0.1.0", + "vue-router": "^4.5.1" + }, + "bin": { + "nuxi": "bin/nuxt.mjs", + "nuxt": "bin/nuxt.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@parcel/watcher": "^2.1.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependenciesMeta": { + "@parcel/watcher": { + "optional": true + }, + "@types/node": { + "optional": true + } + } + }, + "node_modules/nuxt/node_modules/@nuxt/schema": { + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-3.19.2.tgz", + "integrity": "sha512-kMN2oIfrsMc8ACrRweYRG7Q44/KuHG5y7L+4szQhfOgN78OiYkxiM/nSsLH0K2bJq8Eavg+WGfgACj4Lsy+YqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "^3.5.21", + "consola": "^3.4.2", + "defu": "^6.1.4", + "pathe": "^2.0.3", + "pkg-types": "^2.3.0", + "std-env": "^3.9.0", + "ufo": "1.6.1" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/nypm": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", + "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.3.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/ofetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz", + "integrity": "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==", + "dev": true, + "license": "MIT", + "dependencies": { + "destr": "^2.0.3", + "node-fetch-native": "^1.6.4", + "ufo": "^1.5.4" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-change": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/on-change/-/on-change-5.0.1.tgz", + "integrity": "sha512-n7THCP7RkyReRSLkJb8kUWoNsxUIBxTkIp3JKno+sEz6o/9AJ3w3P9fzQkITEkMwyTKJjZciF3v/pVoouxZZMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/on-change?sponsor=1" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/oxc-minify": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/oxc-minify/-/oxc-minify-0.87.0.tgz", + "integrity": "sha512-+UHWp6+0mdq0S2rEsZx9mqgL6JnG9ogO+CU17XccVrPUFtISFcZzk/biTn1JdBYFQ3kztof19pv8blMtgStQ2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-minify/binding-android-arm64": "0.87.0", + "@oxc-minify/binding-darwin-arm64": "0.87.0", + "@oxc-minify/binding-darwin-x64": "0.87.0", + "@oxc-minify/binding-freebsd-x64": "0.87.0", + "@oxc-minify/binding-linux-arm-gnueabihf": "0.87.0", + "@oxc-minify/binding-linux-arm-musleabihf": "0.87.0", + "@oxc-minify/binding-linux-arm64-gnu": "0.87.0", + "@oxc-minify/binding-linux-arm64-musl": "0.87.0", + "@oxc-minify/binding-linux-riscv64-gnu": "0.87.0", + "@oxc-minify/binding-linux-s390x-gnu": "0.87.0", + "@oxc-minify/binding-linux-x64-gnu": "0.87.0", + "@oxc-minify/binding-linux-x64-musl": "0.87.0", + "@oxc-minify/binding-wasm32-wasi": "0.87.0", + "@oxc-minify/binding-win32-arm64-msvc": "0.87.0", + "@oxc-minify/binding-win32-x64-msvc": "0.87.0" + } + }, + "node_modules/oxc-parser": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.87.0.tgz", + "integrity": "sha512-uc47XrtHwkBoES4HFgwgfH9sqwAtJXgAIBq4fFBMZ4hWmgVZoExyn+L4g4VuaecVKXkz1bvlaHcfwHAJPQb5Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.87.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm64": "0.87.0", + "@oxc-parser/binding-darwin-arm64": "0.87.0", + "@oxc-parser/binding-darwin-x64": "0.87.0", + "@oxc-parser/binding-freebsd-x64": "0.87.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.87.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.87.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.87.0", + "@oxc-parser/binding-linux-arm64-musl": "0.87.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.87.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.87.0", + "@oxc-parser/binding-linux-x64-gnu": "0.87.0", + "@oxc-parser/binding-linux-x64-musl": "0.87.0", + "@oxc-parser/binding-wasm32-wasi": "0.87.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.87.0", + "@oxc-parser/binding-win32-x64-msvc": "0.87.0" + } + }, + "node_modules/oxc-transform": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/oxc-transform/-/oxc-transform-0.87.0.tgz", + "integrity": "sha512-dt6INKWY2DKbSc8yR9VQoqBsCjPQ3z/SKv882UqlwFve+K38xtpi2avDlvNd35SpHUwDLDFoV3hMX0U3qOSaaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-transform/binding-android-arm64": "0.87.0", + "@oxc-transform/binding-darwin-arm64": "0.87.0", + "@oxc-transform/binding-darwin-x64": "0.87.0", + "@oxc-transform/binding-freebsd-x64": "0.87.0", + "@oxc-transform/binding-linux-arm-gnueabihf": "0.87.0", + "@oxc-transform/binding-linux-arm-musleabihf": "0.87.0", + "@oxc-transform/binding-linux-arm64-gnu": "0.87.0", + "@oxc-transform/binding-linux-arm64-musl": "0.87.0", + "@oxc-transform/binding-linux-riscv64-gnu": "0.87.0", + "@oxc-transform/binding-linux-s390x-gnu": "0.87.0", + "@oxc-transform/binding-linux-x64-gnu": "0.87.0", + "@oxc-transform/binding-linux-x64-musl": "0.87.0", + "@oxc-transform/binding-wasm32-wasi": "0.87.0", + "@oxc-transform/binding-win32-arm64-msvc": "0.87.0", + "@oxc-transform/binding-win32-x64-msvc": "0.87.0" + } + }, + "node_modules/oxc-walker": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/oxc-walker/-/oxc-walker-0.5.2.tgz", + "integrity": "sha512-XYoZqWwApSKUmSDEFeOKdy3Cdh95cOcSU8f7yskFWE4Rl3cfL5uwyY+EV7Brk9mdNLy+t5SseJajd6g7KncvlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-regexp": "^0.10.0" + }, + "peerDependencies": { + "oxc-parser": ">=0.72.0" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/package-manager-detector": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz", + "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-path": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.1.0.tgz", + "integrity": "sha512-EuCycjZtfPcjWk7KTksnJ5xPMvWGA/6i4zrLYhRG0hGvC3GPU/jGUj3Cy+ZR0v30duV3e23R95T1lE2+lsndSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "protocols": "^2.0.0" + } + }, + "node_modules/parse-url": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-9.2.0.tgz", + "integrity": "sha512-bCgsFI+GeGWPAvAiUv63ZorMeif3/U0zaXABGJbOWt5OH2KCaPHF6S+0ok4aqM9RuIPGyZdx9tR9l13PsW4AYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-path": "^7.0.0", + "parse-path": "^7.0.0" + }, + "engines": { + "node": ">=14.13.0" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz", + "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-calc": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.1.1.tgz", + "integrity": "sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12 || ^20.9 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.38" + } + }, + "node_modules/postcss-colormin": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.4.tgz", + "integrity": "sha512-ziQuVzQZBROpKpfeDwmrG+Vvlr0YWmY/ZAk99XD+mGEBuEojoFekL41NCsdhyNUtZI7DPOoIWIR7vQQK9xwluw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-convert-values": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.7.tgz", + "integrity": "sha512-HR9DZLN04Xbe6xugRH6lS4ZQH2zm/bFh/ZyRkpedZozhvh+awAfbA0P36InO4fZfDhvYfNJeNvlTf1sjwGbw/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-discard-comments": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.4.tgz", + "integrity": "sha512-6tCUoql/ipWwKtVP/xYiFf1U9QgJ0PUvxN7pTcsQ8Ns3Fnwq1pU5D5s1MhT/XySeLq6GXNvn37U46Ded0TckWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.1.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.2.tgz", + "integrity": "sha512-eTonaQvPZ/3i1ASDHOKkYwAybiM45zFIc7KXils4mQmHLqIswXD9XNOKEVxtTFnsmwYzF66u4LMgSr0abDlh5w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-discard-empty": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.1.tgz", + "integrity": "sha512-cFrJKZvcg/uxB6Ijr4l6qmn3pXQBna9zyrPC+sK0zjbkDUZew+6xDltSF7OeB7rAtzaaMVYSdbod+sZOCWnMOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.1.tgz", + "integrity": "sha512-7c3MMjjSZ/qYrx3uc1940GSOzN1Iqjtlqe8uoSg+qdVPYyRb0TILSqqmtlSFuE4mTDECwsm397Ya7iXGzfF7lg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.5.tgz", + "integrity": "sha512-Kpu5v4Ys6QI59FxmxtNB/iHUVDn9Y9sYw66D6+SZoIk4QTz1prC4aYkhIESu+ieG1iylod1f8MILMs1Em3mmIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^7.0.5" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-merge-rules": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.6.tgz", + "integrity": "sha512-2jIPT4Tzs8K87tvgCpSukRQ2jjd+hH6Bb8rEEOUDmmhOeTcqDg5fEFK8uKIu+Pvc3//sm3Uu6FRqfyv7YF7+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^5.0.1", + "postcss-selector-parser": "^7.1.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.1.tgz", + "integrity": "sha512-2m1uiuJeTplll+tq4ENOQSzB8LRnSUChBv7oSyFLsJRtUgAAJGP6LLz0/8lkinTgxrmJSPOEhgY1bMXOQ4ZXhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.1.tgz", + "integrity": "sha512-X9JjaysZJwlqNkJbUDgOclyG3jZEpAMOfof6PUZjPnPrePnPG62pS17CjdM32uT1Uq1jFvNSff9l7kNbmMSL2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "colord": "^2.9.3", + "cssnano-utils": "^5.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-minify-params": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.4.tgz", + "integrity": "sha512-3OqqUddfH8c2e7M35W6zIwv7jssM/3miF9cbCSb1iJiWvtguQjlxZGIHK9JRmc8XAKmE2PFGtHSM7g/VcW97sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1", + "cssnano-utils": "^5.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.5.tgz", + "integrity": "sha512-x2/IvofHcdIrAm9Q+p06ZD1h6FPcQ32WtCRVodJLDR+WMn8EVHI1kvLxZuGKz/9EY5nAmI6lIQIrpo4tBy5+ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "postcss-selector-parser": "^7.1.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.1.tgz", + "integrity": "sha512-sn413ofhSQHlZFae//m9FTOfkmiZ+YQXsbosqOWRiVQncU2BA3daX3n0VF3cG6rGLSFVc5Di/yns0dFfh8NFgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.1.tgz", + "integrity": "sha512-E5nnB26XjSYz/mGITm6JgiDpAbVuAkzXwLzRZtts19jHDUBFxZ0BkXAehy0uimrOjYJbocby4FVswA/5noOxrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.1.tgz", + "integrity": "sha512-pB/SzrIP2l50ZIYu+yQZyMNmnAcwyYb9R1fVWPRxm4zcUFCY2ign7rcntGFuMXDdd9L2pPNUgoODDk91PzRZuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.1.tgz", + "integrity": "sha512-NsSQJ8zj8TIDiF0ig44Byo3Jk9e4gNt9x2VIlJudnQQ5DhWAHJPF4Tr1ITwyHio2BUi/I6Iv0HRO7beHYOloYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-normalize-string": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.1.tgz", + "integrity": "sha512-QByrI7hAhsoze992kpbMlJSbZ8FuCEc1OT9EFbZ6HldXNpsdpZr+YXC5di3UEv0+jeZlHbZcoCADgb7a+lPmmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.1.tgz", + "integrity": "sha512-bHifyuuSNdKKsnNJ0s8fmfLMlvsQwYVxIoUBnowIVl2ZAdrkYQNGVB4RxjfpvkMjipqvbz0u7feBZybkl/6NJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.4.tgz", + "integrity": "sha512-LvIURTi1sQoZqj8mEIE8R15yvM+OhbR1avynMtI9bUzj5gGKR/gfZFd8O7VMj0QgJaIFzxDwxGl/ASMYAkqO8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-normalize-url": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.1.tgz", + "integrity": "sha512-sUcD2cWtyK1AOL/82Fwy1aIVm/wwj5SdZkgZ3QiUzSzQQofrbq15jWJ3BA7Z+yVRwamCjJgZJN0I9IS7c6tgeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.1.tgz", + "integrity": "sha512-vsbgFHMFQrJBJKrUFJNZ2pgBeBkC2IvvoHjz1to0/0Xk7sII24T0qFOiJzG6Fu3zJoq/0yI4rKWi7WhApW+EFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-ordered-values": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.2.tgz", + "integrity": "sha512-AMJjt1ECBffF7CEON/Y0rekRLS6KsePU6PRP08UqYW4UGFRnTXNrByUzYK1h8AC7UWTZdQ9O3Oq9kFIhm0SFEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-utils": "^5.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.4.tgz", + "integrity": "sha512-rdIC9IlMBn7zJo6puim58Xd++0HdbvHeHaPgXsimMfG1ijC5A9ULvNLSE0rUKVJOvNMcwewW4Ga21ngyJjY/+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.1.tgz", + "integrity": "sha512-MhyEbfrm+Mlp/36hvZ9mT9DaO7dbncU0CvWI8V93LRkY6IYlu38OPg3FObnuKTUxJ4qA8HpurdQOo5CyqqO76g==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.1.0.tgz", + "integrity": "sha512-KnAlfmhtoLz6IuU3Sij2ycusNs4jPW+QoFE5kuuUOK8awR6tMxZQrs5Ey3BUz7nFCzT3eqyFgqkyrHiaU2xx3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^4.0.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.4.tgz", + "integrity": "sha512-pmlZjsmEAG7cHd7uK3ZiNSW6otSZ13RHuZ/4cDN/bVglS5EpF2r2oxY99SuOHa8m7AWoBCelTS3JPpzsIs8skQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.1.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "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/postcss/node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-bytes": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-7.1.0.tgz", + "integrity": "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/protocols": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz", + "integrity": "sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dev": true, + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.1.tgz", + "integrity": "sha512-/vFSi3I+ya/D75UZh5GxLc/6UQ+KoKPEvL9autr1yGcaeWzXBQr1tTXmNDS4FImFCPwBAvVe7j9YzR8PQ5rfqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.1", + "@rollup/rollup-android-arm64": "4.52.1", + "@rollup/rollup-darwin-arm64": "4.52.1", + "@rollup/rollup-darwin-x64": "4.52.1", + "@rollup/rollup-freebsd-arm64": "4.52.1", + "@rollup/rollup-freebsd-x64": "4.52.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.1", + "@rollup/rollup-linux-arm-musleabihf": "4.52.1", + "@rollup/rollup-linux-arm64-gnu": "4.52.1", + "@rollup/rollup-linux-arm64-musl": "4.52.1", + "@rollup/rollup-linux-loong64-gnu": "4.52.1", + "@rollup/rollup-linux-ppc64-gnu": "4.52.1", + "@rollup/rollup-linux-riscv64-gnu": "4.52.1", + "@rollup/rollup-linux-riscv64-musl": "4.52.1", + "@rollup/rollup-linux-s390x-gnu": "4.52.1", + "@rollup/rollup-linux-x64-gnu": "4.52.1", + "@rollup/rollup-linux-x64-musl": "4.52.1", + "@rollup/rollup-openharmony-arm64": "4.52.1", + "@rollup/rollup-win32-arm64-msvc": "4.52.1", + "@rollup/rollup-win32-ia32-msvc": "4.52.1", + "@rollup/rollup-win32-x64-gnu": "4.52.1", + "@rollup/rollup-win32-x64-msvc": "4.52.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-visualizer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-6.0.3.tgz", + "integrity": "sha512-ZU41GwrkDcCpVoffviuM9Clwjy5fcUxlz0oMoTXTYsK+tcIFzbdacnrr2n8TXcHxbGKKXtOdjxM2HUS4HjkwIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "open": "^8.0.0", + "picomatch": "^4.0.2", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "rolldown": "1.x || ^1.0.0-beta", + "rollup": "2.x || 3.x || 4.x" + }, + "peerDependenciesMeta": { + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.93.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.1.tgz", + "integrity": "sha512-wLAeLB7IksO2u+cCfhHqcy7/2ZUMPp/X2oV6+LjmweTqgjhOKrkaE/Q1wljxtco5EcOcupZ4c981X0gpk5Tiag==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-loader": { + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.5.2.tgz", + "integrity": "sha512-vMUoSNOUKJILHpcNCCyD23X34gve1TS7Rjd9uXHeKqhvBG39x6XbswFDtpbTElj6XdMFezoWhkh5vtKudf2cgQ==", + "license": "MIT", + "dependencies": { + "klona": "^2.0.4", + "loader-utils": "^2.0.0", + "neo-async": "^2.6.2", + "schema-utils": "^3.0.0", + "semver": "^7.3.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "webpack": "^4.36.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-placeholder": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/serve-placeholder/-/serve-placeholder-2.0.2.tgz", + "integrity": "sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-git": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.28.0.tgz", + "integrity": "sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/structured-clone-es": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/structured-clone-es/-/structured-clone-es-1.0.0.tgz", + "integrity": "sha512-FL8EeKFFyNQv5cMnXI31CIMCsFarSVI2bF0U0ImeNE3g/F1IvJQyqzOXxPBRXiwQfyBTlbNe88jh1jFW0O/jiQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/stylehacks": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.6.tgz", + "integrity": "sha512-iitguKivmsueOmTO0wmxURXBP8uqOO+zikLGZ7Mm9e/94R4w5T999Js2taS/KBOnQ/wdC3jN3vNSrkGDrlnqQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1", + "postcss-selector-parser": "^7.1.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/superjson": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svgo": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", + "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.4.1" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/system-architecture": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", + "integrity": "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tapable": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.4.tgz", + "integrity": "sha512-O1z7ajPkjTgEgmTGz0v9X4eqeEXTDREPTO77pVC1Nbs86feBU1Zhdg+edzavPmYW1olxkwsqA2v4uOw6E8LeDg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/terser": { + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-level-regexp": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/type-level-regexp/-/type-level-regexp-0.1.17.tgz", + "integrity": "sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ultrahtml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz", + "integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/unctx": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/unctx/-/unctx-2.4.1.tgz", + "integrity": "sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17", + "unplugin": "^2.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "license": "MIT", + "peer": true + }, + "node_modules/unenv": { + "version": "2.0.0-rc.21", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.21.tgz", + "integrity": "sha512-Wj7/AMtE9MRnAXa6Su3Lk0LNCfqDYgfwVjwRFVum9U7wsto1imuHqk4kTm7Jni+5A0Hn7dttL6O/zjvUvoo+8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "exsolve": "^1.0.7", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "ufo": "^1.6.1" + } + }, + "node_modules/unhead": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/unhead/-/unhead-2.0.17.tgz", + "integrity": "sha512-xX3PCtxaE80khRZobyWCVxeFF88/Tg9eJDcJWY9us727nsTC7C449B8BUfVBmiF2+3LjPcmqeoB2iuMs0U4oJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookable": "^5.5.3" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unimport": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-5.3.0.tgz", + "integrity": "sha512-cty7t1DESgm0OPfCy9oyn5u9B5t0tMW6tH6bXTjAGIO3SkJsbg/DXYHjrPrUKqultqbAAoltAfYsuu/FEDocjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "local-pkg": "^1.1.2", + "magic-string": "^0.30.19", + "mlly": "^1.8.0", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "pkg-types": "^2.3.0", + "scule": "^1.3.0", + "strip-literal": "^3.0.0", + "tinyglobby": "^0.2.15", + "unplugin": "^2.3.10", + "unplugin-utils": "^0.3.0" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unimport/node_modules/unplugin-utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.0.tgz", + "integrity": "sha512-JLoggz+PvLVMJo+jZt97hdIIIZ2yTzGgft9e9q8iMrC4ewufl62ekeW7mixBghonn2gVb/ICjyvlmOCUBnJLQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/unplugin": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.10.tgz", + "integrity": "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin-utils": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.2.5.tgz", + "integrity": "sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/unplugin-vue-router": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/unplugin-vue-router/-/unplugin-vue-router-0.15.0.tgz", + "integrity": "sha512-PyGehCjd9Ny9h+Uer4McbBjjib3lHihcyUEILa7pHKl6+rh8N7sFyw4ZkV+N30Oq2zmIUG7iKs3qpL0r+gXAaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue-macros/common": "3.0.0-beta.16", + "@vue/language-core": "^3.0.1", + "ast-walker-scope": "^0.8.1", + "chokidar": "^4.0.3", + "json5": "^2.2.3", + "local-pkg": "^1.1.1", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "muggle-string": "^0.4.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "scule": "^1.3.0", + "tinyglobby": "^0.2.14", + "unplugin": "^2.3.5", + "unplugin-utils": "^0.2.4", + "yaml": "^2.8.0" + }, + "peerDependencies": { + "@vue/compiler-sfc": "^3.5.17", + "vue-router": "^4.5.1" + }, + "peerDependenciesMeta": { + "vue-router": { + "optional": true + } + } + }, + "node_modules/unstorage": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.1.tgz", + "integrity": "sha512-KKGwRTT0iVBCErKemkJCLs7JdxNVfqTPc/85ae1XES0+bsHbc/sFBfVi5kJp156cc51BHinIH2l3k0EZ24vOBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^4.0.3", + "destr": "^2.0.5", + "h3": "^1.15.4", + "lru-cache": "^10.4.3", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.4.1", + "ufo": "^1.6.1" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6.0.3 || ^7.0.0", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1.0.1", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/unstorage/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/untun": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/untun/-/untun-0.1.3.tgz", + "integrity": "sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.5", + "consola": "^3.2.3", + "pathe": "^1.1.1" + }, + "bin": { + "untun": "bin/untun.mjs" + } + }, + "node_modules/untun/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/untyped": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/untyped/-/untyped-2.0.0.tgz", + "integrity": "sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "defu": "^6.1.4", + "jiti": "^2.4.2", + "knitwork": "^1.2.0", + "scule": "^1.3.0" + }, + "bin": { + "untyped": "dist/cli.mjs" + } + }, + "node_modules/unwasm": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/unwasm/-/unwasm-0.3.11.tgz", + "integrity": "sha512-Vhp5gb1tusSQw5of/g3Q697srYgMXvwMgXMjcG4ZNga02fDX9coxJ9fAb0Ci38hM2Hv/U1FXRPGgjP2BYqhNoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "knitwork": "^1.2.0", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "pathe": "^2.0.3", + "pkg-types": "^2.2.0", + "unplugin": "^2.3.6" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "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/uqr": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.2.tgz", + "integrity": "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", + "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-dev-rpc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vite-dev-rpc/-/vite-dev-rpc-1.1.0.tgz", + "integrity": "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==", + "dev": true, + "license": "MIT", + "dependencies": { + "birpc": "^2.4.0", + "vite-hot-client": "^2.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0" + } + }, + "node_modules/vite-hot-client": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-2.1.0.tgz", + "integrity": "sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-plugin-checker": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.10.3.tgz", + "integrity": "sha512-f4sekUcDPF+T+GdbbE8idb1i2YplBAoH+SfRS0e/WRBWb2rYb1Jf5Pimll0Rj+3JgIYWwG2K5LtBPCXxoibkLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "chokidar": "^4.0.3", + "npm-run-path": "^6.0.0", + "picocolors": "^1.1.1", + "picomatch": "^4.0.3", + "strip-ansi": "^7.1.0", + "tiny-invariant": "^1.3.3", + "tinyglobby": "^0.2.14", + "vscode-uri": "^3.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "peerDependencies": { + "@biomejs/biome": ">=1.7", + "eslint": ">=7", + "meow": "^13.2.0", + "optionator": "^0.9.4", + "stylelint": ">=16", + "typescript": "*", + "vite": ">=2.0.0", + "vls": "*", + "vti": "*", + "vue-tsc": "~2.2.10 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@biomejs/biome": { + "optional": true + }, + "eslint": { + "optional": true + }, + "meow": { + "optional": true + }, + "optionator": { + "optional": true + }, + "stylelint": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vls": { + "optional": true + }, + "vti": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/vite-plugin-checker/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-checker/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-inspect": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-11.3.3.tgz", + "integrity": "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.1.0", + "debug": "^4.4.1", + "error-stack-parser-es": "^1.0.5", + "ohash": "^2.0.11", + "open": "^10.2.0", + "perfect-debounce": "^2.0.0", + "sirv": "^3.0.1", + "unplugin-utils": "^0.3.0", + "vite-dev-rpc": "^1.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite-plugin-inspect/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-inspect/node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-inspect/node_modules/unplugin-utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.0.tgz", + "integrity": "sha512-JLoggz+PvLVMJo+jZt97hdIIIZ2yTzGgft9e9q8iMrC4ewufl62ekeW7mixBghonn2gVb/ICjyvlmOCUBnJLQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/vite-plugin-vue-tracer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-tracer/-/vite-plugin-vue-tracer-1.0.0.tgz", + "integrity": "sha512-a+UB9IwGx5uwS4uG/a9kM6fCMnxONDkOTbgCUbhFpiGhqfxrrC1+9BibV7sWwUnwj1Dg6MnRxG0trLgUZslDXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^3.0.3", + "exsolve": "^1.0.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "source-map-js": "^1.2.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0", + "vue": "^3.5.0" + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.21.tgz", + "integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.21", + "@vue/compiler-sfc": "3.5.21", + "@vue/runtime-dom": "3.5.21", + "@vue/server-renderer": "3.5.21", + "@vue/shared": "3.5.21" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-bundle-renderer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/vue-bundle-renderer/-/vue-bundle-renderer-2.1.2.tgz", + "integrity": "sha512-M4WRBO/O/7G9phGaGH9AOwOnYtY9ZpPoDVpBpRzR2jO5rFL9mgIlQIgums2ljCTC2HL1jDXFQc//CzWcAQHgAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ufo": "^1.6.1" + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-devtools-stub": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/vue-devtools-stub/-/vue-devtools-stub-0.1.0.tgz", + "integrity": "sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.0.8.tgz", + "integrity": "sha512-H9yg/m6ywykmWS+pIAEs65v2FrVm5uOA0a0dHkX6Sx8dNg1a1m4iudt/6eGa9fAenmNHGlLFN9XpWQb8i5sU1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.23", + "@vue/language-core": "3.0.8" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/vue3-highlightjs": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/vue3-highlightjs/-/vue3-highlightjs-1.0.5.tgz", + "integrity": "sha512-Q4YNPXu0X5VMBnwPVOk+IQf1Ohp9jFdMitEAmzaz8qVVefcQpN6Dx4BnDGKxja3TLDVF+EgL136wC8YzmoCX9w==", + "license": "ISC", + "dependencies": { + "highlight.js": "^10.3.2" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vue3-highlightjs/node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/vuetify": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.10.2.tgz", + "integrity": "sha512-uuAm+uj170ZWdS0aiomhgtdYOR3/I2yzynskBfYyFB1Fn/HKvlvPDaU3ISZnf0GHIJQENdj7eLg/wTChrIwEQw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/johnleider" + }, + "peerDependencies": { + "typescript": ">=4.7", + "vite-plugin-vuetify": ">=2.1.0", + "vue": "^3.5.0", + "webpack-plugin-vuetify": ">=3.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vite-plugin-vuetify": { + "optional": true + }, + "webpack-plugin-vuetify": { + "optional": true + } + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "license": "MIT", + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.101.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", + "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.11", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.11.tgz", + "integrity": "sha512-sQi6PERyO/mT8w564ojOVeAlYTtVQmC2GaktQAf+IdI75/GKIggosBuvyVXvEV+FATAT6RbLdIjFoiIId4ozoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/examples/clientsocket/package.json b/examples/clientsocket/package.json new file mode 100644 index 0000000..8a35587 --- /dev/null +++ b/examples/clientsocket/package.json @@ -0,0 +1,27 @@ +{ + "name": "nuxt3-websocket-client", + "private": true, + "type": "module", + "scripts": { + "build": "nuxt build", + "dev": "nuxt dev", + "generate": "nuxt generate", + "preview": "nuxt preview", + "postinstall": "nuxt prepare" + }, + "devDependencies": { + "@nuxt/devtools": "latest", + "nuxt": "^3.8.0", + "vue": "^3.3.8", + "vue-router": "^4.2.5", + "vue-tsc": "^3.0.8" + }, + "dependencies": { + "@mdi/font": "^7.3.67", + "@nuxtjs/vuetify": "^1.12.3", + "highlight.js": "^11.9.0", + "pinia": "^2.1.7", + "vue3-highlightjs": "^1.0.5", + "vuetify": "^3.4.0" + } +} diff --git a/examples/clientsocket/pages/index.vue b/examples/clientsocket/pages/index.vue new file mode 100644 index 0000000..536c44d --- /dev/null +++ b/examples/clientsocket/pages/index.vue @@ -0,0 +1,14 @@ + + + + + diff --git a/examples/clientsocket/plugins/vuetify.js b/examples/clientsocket/plugins/vuetify.js new file mode 100644 index 0000000..a1aa056 --- /dev/null +++ b/examples/clientsocket/plugins/vuetify.js @@ -0,0 +1,15 @@ +import { createVuetify } from 'vuetify' +import * as components from 'vuetify/components' +import * as directives from 'vuetify/directives' + +export default defineNuxtPlugin((nuxtApp) => { + const vuetify = createVuetify({ + components, + directives, + theme: { + defaultTheme: 'light' + } + }) + + nuxtApp.vueApp.use(vuetify) +}) diff --git a/examples/clientsocket/public/favicon.ico b/examples/clientsocket/public/favicon.ico new file mode 100644 index 0000000..18993ad Binary files /dev/null and b/examples/clientsocket/public/favicon.ico differ diff --git a/examples/clientsocket/public/robots.txt b/examples/clientsocket/public/robots.txt new file mode 100644 index 0000000..0ad279c --- /dev/null +++ b/examples/clientsocket/public/robots.txt @@ -0,0 +1,2 @@ +User-Agent: * +Disallow: diff --git a/examples/clientsocket/tsconfig.json b/examples/clientsocket/tsconfig.json new file mode 100644 index 0000000..4b34df1 --- /dev/null +++ b/examples/clientsocket/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.nuxt/tsconfig.json" +} diff --git a/examples/clientsocket/types/websocket.ts b/examples/clientsocket/types/websocket.ts new file mode 100644 index 0000000..98720db --- /dev/null +++ b/examples/clientsocket/types/websocket.ts @@ -0,0 +1,182 @@ +export interface WebSocketMessage { + type: string; + data: any; + timestamp?: number; + client_id?: string; + message_id?: string; +} + +export interface ConnectionInfo { + client_id: string; + static_id: string; + ip_address: string; + room: string; + user_id: string; + connected_at: number; + id_type: string; +} + +export interface ClientInfo { + id: string; + static_id: string; + ip_address: string; + user_id: string; + room: string; + connected_at: number; + last_ping: number; + is_active?: boolean; +} + +export interface OnlineUser { + client_id: string; + static_id: string; + user_id: string; + room: string; + ip_address: string; + connected_at: number; + last_ping: number; +} + +export interface ConnectionStats { + connected_clients: number; + unique_ips: number; + static_clients: number; + active_rooms: number; + ip_distribution: Record; + room_distribution: Record; + message_queue_size: number; + queue_workers: number; + uptime: number; + timestamp: number; +} + +export interface SystemHealth { + databases: any; + available_dbs: string[]; + websocket_status: string; + uptime_seconds: number; +} + +export interface PerformanceMetrics { + messages_per_second: number; + average_latency_ms: number; + error_rate_percent: number; + memory_usage_bytes: number; +} + +export interface MonitoringData { + stats: ConnectionStats; + recent_activity: ActivityLog[]; + system_health: SystemHealth; + performance: PerformanceMetrics; +} + +export interface ActivityLog { + timestamp: number; + event: string; + client_id: string; + details: string; +} + +export interface MessageHistory { + timestamp: Date; + type: string; + data: any; + messageId?: string; + size: number; + icon?: string; + timeString?: string; +} + +export interface ConnectionState { + isConnected: boolean; + isConnecting: boolean; + connectionStatus: "disconnected" | "connecting" | "connected" | "error"; + clientId: string | null; + staticId: string | null; + currentRoom: string | null; + userId: string; + ipAddress: string | null; + connectionStartTime: number | null; + lastPingTime: number | null; + connectionLatency: number; + connectionHealth: "poor" | "warning" | "good" | "excellent"; + reconnectAttempts: number; + messagesReceived: number; + messagesSent: number; + uptime: string; +} + +export interface WebSocketConfig { + wsUrl: string; + userId: string; + room: string; + staticId?: string; + useIPBasedId?: boolean; + autoReconnect: boolean; + heartbeatEnabled: boolean; + maxReconnectAttempts: number; + reconnectDelay: number; + maxReconnectDelay: number; + heartbeatInterval: number; + heartbeatTimeout: number; + maxMissedHeartbeats: number; + maxMessages: number; + messageWarningThreshold: number; + actionThrottle: number; +} + +export type MessageType = + | "welcome" + | "broadcast" + | "direct_message" + | "room_message" + | "ping" + | "pong" + | "heartbeat" + | "heartbeat_ack" + | "connection_test" + | "connection_test_result" + | "get_online_users" + | "online_users" + | "get_server_info" + | "server_info" + | "error" + | "message_received" + | "broadcast_sent" + | "direct_message_sent" + | "room_message_sent" + | "db_insert" + | "db_query" + | "db_custom_query" + | "query_result" + | "admin_kick_client" + | "admin_kill_server" + | "get_server_stats" + | "get_system_health" + | "admin_clear_logs" + | "get_stats" + | "get_room_info" + | "join_room" + | "leave_room" + | "database_change" + | "data_stream" + | "server_heartbeat" + | "system_status" + | "clients_by_ip" + | "client_info" + | "get_clients_by_ip" + | "get_client_info" + | "health_check" + | "database_list" + | "connection_stats" + | "trigger_notification" + | "notification_sent" + | "API_TEST" + | "manual_test" + | "retribusi_created" + | "retribusi_updated" + | "retribusi_deleted" + | "peserta_changes" + | "retribusi_changes" + | "system_changes"; diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5dd4cb6 --- /dev/null +++ b/go.mod @@ -0,0 +1,90 @@ +module api-service + +go 1.24.4 + +require ( + github.com/gin-gonic/gin v1.10.1 + github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.1 + github.com/jackc/pgx/v5 v5.7.2 // Ensure pgx is a direct dependency + go.mongodb.org/mongo-driver v1.17.3 + golang.org/x/crypto v0.41.0 + golang.org/x/sync v0.16.0 + gorm.io/driver/mysql v1.6.0 // GORM MySQL driver + gorm.io/driver/postgres v1.5.11 // Added GORM PostgreSQL driver + gorm.io/driver/sqlserver v1.6.1 // GORM SQL Server driver +) + +require ( + github.com/daku10/go-lz-string v0.0.6 + github.com/go-playground/validator/v10 v10.27.0 + github.com/go-sql-driver/mysql v1.8.1 + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 + github.com/mashingan/smapping v0.1.19 + github.com/rs/zerolog v1.34.0 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.6 + github.com/tidwall/gjson v1.18.0 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/microsoft/go-mssqldb v1.8.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/mod v0.26.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/tools v0.35.0 // indirect + google.golang.org/protobuf v1.36.7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/gorm v1.30.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7ecaabb --- /dev/null +++ b/go.sum @@ -0,0 +1,361 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/daku10/go-lz-string v0.0.6 h1:aO8FFp4QPuNp7+WNyh1DyNjGF3UbZu95tUv9xOZNsYQ= +github.com/daku10/go-lz-string v0.0.6/go.mod h1:Vk++rSG3db8HXJaHEAbxiy/ukjTmPBw/iI+SrVZDzfs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= +github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mashingan/smapping v0.1.19 h1:SsEtuPn2UcM1croIupPtGLgWgpYRuS0rSQMvKD9g2BQ= +github.com/mashingan/smapping v0.1.19/go.mod h1:FjfiwFxGOuNxL/OT1WcrNAwTPx0YJeg5JiXwBB1nyig= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/microsoft/go-mssqldb v1.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I= +github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= +go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/sqlserver v1.6.1 h1:XWISFsu2I2pqd1KJhhTZNJMx1jNQ+zVL/Q8ovDcUjtY= +gorm.io/driver/sqlserver v1.6.1/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..f34deb4 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,739 @@ +package config + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "log" + "os" + "strconv" + "strings" + "time" + + "github.com/go-playground/validator/v10" +) + +type Config struct { + Server ServerConfig + Databases map[string]DatabaseConfig + ReadReplicas map[string][]DatabaseConfig // For read replicas + Keycloak KeycloakConfig + Bpjs BpjsConfig + SatuSehat SatuSehatConfig + Swagger SwaggerConfig + Validator *validator.Validate +} + +type SwaggerConfig struct { + Title string + Description string + Version string + TermsOfService string + ContactName string + ContactURL string + ContactEmail string + LicenseName string + LicenseURL string + Host string + BasePath string + Schemes []string +} + +type ServerConfig struct { + Port int + Mode string +} + +type DatabaseConfig struct { + Name string + Type string // postgres, mysql, sqlserver, sqlite, mongodb + Host string + Port int + Username string + Password string + Database string + Schema string + SSLMode string + Path string // For SQLite + Options string // Additional connection options + MaxOpenConns int // Max open connections + MaxIdleConns int // Max idle connections + ConnMaxLifetime time.Duration // Connection max lifetime +} + +type KeycloakConfig struct { + Issuer string + Audience string + JwksURL string + Enabled bool +} + +type BpjsConfig struct { + BaseURL string `json:"base_url"` + ConsID string `json:"cons_id"` + UserKey string `json:"user_key"` + SecretKey string `json:"secret_key"` + Timeout time.Duration `json:"timeout"` +} + +type SatuSehatConfig struct { + OrgID string `json:"org_id"` + FasyakesID string `json:"fasyakes_id"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + AuthURL string `json:"auth_url"` + BaseURL string `json:"base_url"` + ConsentURL string `json:"consent_url"` + KFAURL string `json:"kfa_url"` + Timeout time.Duration `json:"timeout"` +} + +// SetHeader generates required headers for BPJS VClaim API +// func (cfg BpjsConfig) SetHeader() (string, string, string, string, string) { +// timenow := time.Now().UTC() +// t, err := time.Parse(time.RFC3339, "1970-01-01T00:00:00Z") +// if err != nil { +// log.Fatal(err) +// } + +// tstamp := timenow.Unix() - t.Unix() +// secret := []byte(cfg.SecretKey) +// message := []byte(cfg.ConsID + "&" + fmt.Sprint(tstamp)) +// hash := hmac.New(sha256.New, secret) +// hash.Write(message) + +// // to lowercase hexits +// hex.EncodeToString(hash.Sum(nil)) +// // to base64 +// xSignature := base64.StdEncoding.EncodeToString(hash.Sum(nil)) + +// return cfg.ConsID, cfg.SecretKey, cfg.UserKey, fmt.Sprint(tstamp), xSignature +// } +func (cfg BpjsConfig) SetHeader() (string, string, string, string, string) { + timenow := time.Now().UTC() + t, err := time.Parse(time.RFC3339, "1970-01-01T00:00:00Z") + if err != nil { + log.Fatal(err) + } + + tstamp := timenow.Unix() - t.Unix() + secret := []byte(cfg.SecretKey) + message := []byte(cfg.ConsID + "&" + fmt.Sprint(tstamp)) + hash := hmac.New(sha256.New, secret) + hash.Write(message) + + // to lowercase hexits + hex.EncodeToString(hash.Sum(nil)) + // to base64 + xSignature := base64.StdEncoding.EncodeToString(hash.Sum(nil)) + + return cfg.ConsID, cfg.SecretKey, cfg.UserKey, fmt.Sprint(tstamp), xSignature +} + +type ConfigBpjs struct { + Cons_id string + Secret_key string + User_key string +} + +// SetHeader for backward compatibility +func (cfg ConfigBpjs) SetHeader() (string, string, string, string, string) { + bpjsConfig := BpjsConfig{ + ConsID: cfg.Cons_id, + SecretKey: cfg.Secret_key, + UserKey: cfg.User_key, + } + return bpjsConfig.SetHeader() +} + +func LoadConfig() *Config { + config := &Config{ + Server: ServerConfig{ + Port: getEnvAsInt("PORT", 8080), + Mode: getEnv("GIN_MODE", "debug"), + }, + Databases: make(map[string]DatabaseConfig), + ReadReplicas: make(map[string][]DatabaseConfig), + Keycloak: KeycloakConfig{ + Issuer: getEnv("KEYCLOAK_ISSUER", "https://keycloak.example.com/auth/realms/yourrealm"), + Audience: getEnv("KEYCLOAK_AUDIENCE", "your-client-id"), + JwksURL: getEnv("KEYCLOAK_JWKS_URL", "https://keycloak.example.com/auth/realms/yourrealm/protocol/openid-connect/certs"), + Enabled: getEnvAsBool("KEYCLOAK_ENABLED", true), + }, + Bpjs: BpjsConfig{ + BaseURL: getEnv("BPJS_BASEURL", "https://apijkn.bpjs-kesehatan.go.id"), + ConsID: getEnv("BPJS_CONSID", ""), + UserKey: getEnv("BPJS_USERKEY", ""), + SecretKey: getEnv("BPJS_SECRETKEY", ""), + Timeout: parseDuration(getEnv("BPJS_TIMEOUT", "30s")), + }, + SatuSehat: SatuSehatConfig{ + OrgID: getEnv("BRIDGING_SATUSEHAT_ORG_ID", ""), + FasyakesID: getEnv("BRIDGING_SATUSEHAT_FASYAKES_ID", ""), + ClientID: getEnv("BRIDGING_SATUSEHAT_CLIENT_ID", ""), + ClientSecret: getEnv("BRIDGING_SATUSEHAT_CLIENT_SECRET", ""), + AuthURL: getEnv("BRIDGING_SATUSEHAT_AUTH_URL", "https://api-satusehat.kemkes.go.id/oauth2/v1"), + BaseURL: getEnv("BRIDGING_SATUSEHAT_BASE_URL", "https://api-satusehat.kemkes.go.id/fhir-r4/v1"), + ConsentURL: getEnv("BRIDGING_SATUSEHAT_CONSENT_URL", "https://api-satusehat.dto.kemkes.go.id/consent/v1"), + KFAURL: getEnv("BRIDGING_SATUSEHAT_KFA_URL", "https://api-satusehat.kemkes.go.id/kfa-v2"), + Timeout: parseDuration(getEnv("BRIDGING_SATUSEHAT_TIMEOUT", "30s")), + }, + Swagger: SwaggerConfig{ + Title: getEnv("SWAGGER_TITLE", "SERVICE API"), + Description: getEnv("SWAGGER_DESCRIPTION", "CUSTUM SERVICE API"), + Version: getEnv("SWAGGER_VERSION", "1.0.0"), + TermsOfService: getEnv("SWAGGER_TERMS_OF_SERVICE", "http://swagger.io/terms/"), + ContactName: getEnv("SWAGGER_CONTACT_NAME", "API Support"), + ContactURL: getEnv("SWAGGER_CONTACT_URL", "http://rssa.example.com/support"), + ContactEmail: getEnv("SWAGGER_CONTACT_EMAIL", "support@swagger.io"), + LicenseName: getEnv("SWAGGER_LICENSE_NAME", "Apache 2.0"), + LicenseURL: getEnv("SWAGGER_LICENSE_URL", "http://www.apache.org/licenses/LICENSE-2.0.html"), + Host: getEnv("SWAGGER_HOST", "localhost:8080"), + BasePath: getEnv("SWAGGER_BASE_PATH", "/api/v1"), + Schemes: parseSchemes(getEnv("SWAGGER_SCHEMES", "http,https")), + }, + } + + // Initialize validator + config.Validator = validator.New() + + // Load database configurations + config.loadDatabaseConfigs() + + // Load read replica configurations + config.loadReadReplicaConfigs() + + return config +} + +func (c *Config) loadDatabaseConfigs() { + // Simplified approach: Directly load from environment variables + // This ensures we get the exact values specified in .env + + // Primary database configuration + c.Databases["default"] = DatabaseConfig{ + Name: "default", + Type: getEnv("DB_CONNECTION", "postgres"), + Host: getEnv("DB_HOST", "localhost"), + Port: getEnvAsInt("DB_PORT", 5432), + Username: getEnv("DB_USERNAME", ""), + Password: getEnv("DB_PASSWORD", ""), + Database: getEnv("DB_DATABASE", "satu_db"), + Schema: getEnv("DB_SCHEMA", "public"), + SSLMode: getEnv("DB_SSLMODE", "disable"), + MaxOpenConns: getEnvAsInt("DB_MAX_OPEN_CONNS", 25), + MaxIdleConns: getEnvAsInt("DB_MAX_IDLE_CONNS", 25), + ConnMaxLifetime: parseDuration(getEnv("DB_CONN_MAX_LIFETIME", "5m")), + } + + // SATUDATA database configuration + c.addPostgreSQLConfigs() + + // MongoDB database configuration + c.addMongoDBConfigs() + + // Legacy support for backward compatibility + envVars := os.Environ() + dbConfigs := make(map[string]map[string]string) + + // Parse database configurations from environment variables + for _, envVar := range envVars { + parts := strings.SplitN(envVar, "=", 2) + if len(parts) != 2 { + continue + } + + key := parts[0] + value := parts[1] + + // Parse specific database configurations + if strings.HasSuffix(key, "_CONNECTION") || strings.HasSuffix(key, "_HOST") || + strings.HasSuffix(key, "_DATABASE") || strings.HasSuffix(key, "_USERNAME") || + strings.HasSuffix(key, "_PASSWORD") || strings.HasSuffix(key, "_PORT") || + strings.HasSuffix(key, "_NAME") { + + segments := strings.Split(key, "_") + if len(segments) >= 2 { + dbName := strings.ToLower(strings.Join(segments[:len(segments)-1], "_")) + property := strings.ToLower(segments[len(segments)-1]) + + if dbConfigs[dbName] == nil { + dbConfigs[dbName] = make(map[string]string) + } + dbConfigs[dbName][property] = value + } + } + } + + // Create DatabaseConfig from parsed configurations for additional databases + for name, config := range dbConfigs { + // Skip empty configurations or system configurations + if name == "" || strings.Contains(name, "chrome_crashpad_pipe") || name == "primary" { + continue + } + + dbConfig := DatabaseConfig{ + Name: name, + Type: getEnvFromMap(config, "connection", getEnvFromMap(config, "type", "postgres")), + Host: getEnvFromMap(config, "host", "localhost"), + Port: getEnvAsIntFromMap(config, "port", 5432), + Username: getEnvFromMap(config, "username", ""), + Password: getEnvFromMap(config, "password", ""), + Database: getEnvFromMap(config, "database", getEnvFromMap(config, "name", name)), + Schema: getEnvFromMap(config, "schema", "public"), + SSLMode: getEnvFromMap(config, "sslmode", "disable"), + Path: getEnvFromMap(config, "path", ""), + Options: getEnvFromMap(config, "options", ""), + MaxOpenConns: getEnvAsIntFromMap(config, "max_open_conns", 25), + MaxIdleConns: getEnvAsIntFromMap(config, "max_idle_conns", 25), + ConnMaxLifetime: parseDuration(getEnvFromMap(config, "conn_max_lifetime", "5m")), + } + + // Skip if username is empty and it's not a system config + if dbConfig.Username == "" && !strings.HasPrefix(name, "chrome") { + continue + } + + c.Databases[name] = dbConfig + } +} + +func (c *Config) loadReadReplicaConfigs() { + envVars := os.Environ() + + for _, envVar := range envVars { + parts := strings.SplitN(envVar, "=", 2) + if len(parts) != 2 { + continue + } + + key := parts[0] + value := parts[1] + + // Parse read replica configurations (format: [DBNAME]_REPLICA_[INDEX]_[PROPERTY]) + if strings.Contains(key, "_REPLICA_") { + segments := strings.Split(key, "_") + if len(segments) >= 5 && strings.ToUpper(segments[2]) == "REPLICA" { + dbName := strings.ToLower(segments[1]) + replicaIndex := segments[3] + property := strings.ToLower(strings.Join(segments[4:], "_")) + + replicaKey := dbName + "_replica_" + replicaIndex + + if c.ReadReplicas[dbName] == nil { + c.ReadReplicas[dbName] = []DatabaseConfig{} + } + + // Find or create replica config + var replicaConfig *DatabaseConfig + for i := range c.ReadReplicas[dbName] { + if c.ReadReplicas[dbName][i].Name == replicaKey { + replicaConfig = &c.ReadReplicas[dbName][i] + break + } + } + + if replicaConfig == nil { + // Create new replica config + newConfig := DatabaseConfig{ + Name: replicaKey, + Type: c.Databases[dbName].Type, + Host: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_HOST", c.Databases[dbName].Host), + Port: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_PORT", c.Databases[dbName].Port), + Username: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_USERNAME", c.Databases[dbName].Username), + Password: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_PASSWORD", c.Databases[dbName].Password), + Database: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_DATABASE", c.Databases[dbName].Database), + Schema: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SCHEMA", c.Databases[dbName].Schema), + SSLMode: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SSLMODE", c.Databases[dbName].SSLMode), + MaxOpenConns: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_OPEN_CONNS", c.Databases[dbName].MaxOpenConns), + MaxIdleConns: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_IDLE_CONNS", c.Databases[dbName].MaxIdleConns), + ConnMaxLifetime: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_CONN_MAX_LIFETIME", "5m")), + } + c.ReadReplicas[dbName] = append(c.ReadReplicas[dbName], newConfig) + replicaConfig = &c.ReadReplicas[dbName][len(c.ReadReplicas[dbName])-1] + } + + // Update the specific replica + switch property { + case "host": + replicaConfig.Host = value + case "port": + replicaConfig.Port = getEnvAsInt(key, 5432) + case "username": + replicaConfig.Username = value + case "password": + replicaConfig.Password = value + case "database": + replicaConfig.Database = value + case "schema": + replicaConfig.Schema = value + case "sslmode": + replicaConfig.SSLMode = value + case "max_open_conns": + replicaConfig.MaxOpenConns = getEnvAsInt(key, 25) + case "max_idle_conns": + replicaConfig.MaxIdleConns = getEnvAsInt(key, 25) + case "conn_max_lifetime": + replicaConfig.ConnMaxLifetime = parseDuration(value) + } + } + } + } +} + +func (c *Config) addSpecificDatabase(prefix, defaultType string) { + connection := getEnv(strings.ToUpper(prefix)+"_CONNECTION", defaultType) + host := getEnv(strings.ToUpper(prefix)+"_HOST", "") + if host != "" { + dbConfig := DatabaseConfig{ + Name: prefix, + Type: connection, + Host: host, + Port: getEnvAsInt(strings.ToUpper(prefix)+"_PORT", 5432), + Username: getEnv(strings.ToUpper(prefix)+"_USERNAME", ""), + Password: getEnv(strings.ToUpper(prefix)+"_PASSWORD", ""), + Database: getEnv(strings.ToUpper(prefix)+"_DATABASE", getEnv(strings.ToUpper(prefix)+"_NAME", prefix)), + Schema: getEnv(strings.ToUpper(prefix)+"_SCHEMA", "public"), + SSLMode: getEnv(strings.ToUpper(prefix)+"_SSLMODE", "disable"), + MaxOpenConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_OPEN_CONNS", 25), + MaxIdleConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_IDLE_CONNS", 25), + ConnMaxLifetime: parseDuration(getEnv(strings.ToUpper(prefix)+"_CONN_MAX_LIFETIME", "5m")), + } + c.Databases[prefix] = dbConfig + } +} + +// PostgreSQL database +func (c *Config) addPostgreSQLConfigs() { + // SATUDATA database configuration + // defaultPOSTGRESHost := getEnv("POSTGRES_HOST", "localhost") + // if defaultPOSTGRESHost != "" { + // c.Databases["postgres"] = DatabaseConfig{ + // Name: "postgres", + // Type: getEnv("POSTGRES_CONNECTION", "postgres"), + // Host: defaultPOSTGRESHost, + // Port: getEnvAsInt("POSTGRES_PORT", 5432), + // Username: getEnv("POSTGRES_USERNAME", ""), + // Password: getEnv("POSTGRES_PASSWORD", ""), + // Database: getEnv("POSTGRES_DATABASE", "postgres"), + // Schema: getEnv("POSTGRES_SCHEMA", "public"), + // SSLMode: getEnv("POSTGRES_SSLMODE", "disable"), + // MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25), + // MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25), + // ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")), + // } + // } + + // Support for custom PostgreSQL configurations with POSTGRES_ prefix + envVars := os.Environ() + for _, envVar := range envVars { + parts := strings.SplitN(envVar, "=", 2) + if len(parts) != 2 { + continue + } + + key := parts[0] + // Parse PostgreSQL configurations (format: POSTGRES_[NAME]_[PROPERTY]) + if strings.HasPrefix(key, "POSTGRES_") && strings.Contains(key, "_") { + segments := strings.Split(key, "_") + if len(segments) >= 3 { + dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_")) + + // Skip if it's a standard PostgreSQL configuration + if dbName == "connection" || dbName == "dev" || dbName == "default" || dbName == "satudata" { + continue + } + + // Create or update PostgreSQL configuration + if _, exists := c.Databases[dbName]; !exists { + c.Databases[dbName] = DatabaseConfig{ + Name: dbName, + Type: "postgres", + Host: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_HOST", "localhost"), + Port: getEnvAsInt("POSTGRES_"+strings.ToUpper(dbName)+"_PORT", 5432), + Username: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_USERNAME", ""), + Password: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_PASSWORD", ""), + Database: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_DATABASE", dbName), + Schema: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SCHEMA", "public"), + SSLMode: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"), + MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25), + MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25), + ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")), + } + } + } + } + } +} + +// addMYSQLConfigs adds MYSQL database +func (c *Config) addMySQLConfigs() { + // Primary MySQL configuration + defaultMySQLHost := getEnv("MYSQL_HOST", "") + if defaultMySQLHost != "" { + c.Databases["mysql"] = DatabaseConfig{ + Name: "mysql", + Type: getEnv("MYSQL_CONNECTION", "mysql"), + Host: defaultMySQLHost, + Port: getEnvAsInt("MYSQL_PORT", 3306), + Username: getEnv("MYSQL_USERNAME", ""), + Password: getEnv("MYSQL_PASSWORD", ""), + Database: getEnv("MYSQL_DATABASE", "mysql"), + SSLMode: getEnv("MYSQL_SSLMODE", "disable"), + MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25), + MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25), + ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")), + } + } + + // Support for custom MySQL configurations with MYSQL_ prefix + envVars := os.Environ() + for _, envVar := range envVars { + parts := strings.SplitN(envVar, "=", 2) + if len(parts) != 2 { + continue + } + + key := parts[0] + // Parse MySQL configurations (format: MYSQL_[NAME]_[PROPERTY]) + if strings.HasPrefix(key, "MYSQL_") && strings.Contains(key, "_") { + segments := strings.Split(key, "_") + if len(segments) >= 3 { + dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_")) + + // Skip if it's a standard MySQL configuration + if dbName == "connection" || dbName == "dev" || dbName == "max" || dbName == "conn" { + continue + } + + // Create or update MySQL configuration + if _, exists := c.Databases[dbName]; !exists { + mysqlHost := getEnv("MYSQL_"+strings.ToUpper(dbName)+"_HOST", "") + if mysqlHost != "" { + c.Databases[dbName] = DatabaseConfig{ + Name: dbName, + Type: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_CONNECTION", "mysql"), + Host: mysqlHost, + Port: getEnvAsInt("MYSQL_"+strings.ToUpper(dbName)+"_PORT", 3306), + Username: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_USERNAME", ""), + Password: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_PASSWORD", ""), + Database: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_DATABASE", dbName), + SSLMode: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"), + MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25), + MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25), + ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")), + } + } + } + } + } + } +} + +// addMongoDBConfigs adds MongoDB database configurations from environment variables +func (c *Config) addMongoDBConfigs() { + // Primary MongoDB configuration + mongoHost := getEnv("MONGODB_HOST", "") + if mongoHost != "" { + c.Databases["mongodb"] = DatabaseConfig{ + Name: "mongodb", + Type: getEnv("MONGODB_CONNECTION", "mongodb"), + Host: mongoHost, + Port: getEnvAsInt("MONGODB_PORT", 27017), + Username: getEnv("MONGODB_USER", ""), + Password: getEnv("MONGODB_PASS", ""), + Database: getEnv("MONGODB_MASTER", "master"), + SSLMode: getEnv("MONGODB_SSLMODE", "disable"), + MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100), + MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10), + ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")), + } + } + + // Additional MongoDB configurations for local database + mongoLocalHost := getEnv("MONGODB_LOCAL_HOST", "") + if mongoLocalHost != "" { + c.Databases["mongodb_local"] = DatabaseConfig{ + Name: "mongodb_local", + Type: getEnv("MONGODB_CONNECTION", "mongodb"), + Host: mongoLocalHost, + Port: getEnvAsInt("MONGODB_LOCAL_PORT", 27017), + Username: getEnv("MONGODB_LOCAL_USER", ""), + Password: getEnv("MONGODB_LOCAL_PASS", ""), + Database: getEnv("MONGODB_LOCAL_DB", "local"), + SSLMode: getEnv("MONGOD_SSLMODE", "disable"), + MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100), + MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10), + ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")), + } + } + + // Support for custom MongoDB configurations with MONGODB_ prefix + envVars := os.Environ() + for _, envVar := range envVars { + parts := strings.SplitN(envVar, "=", 2) + if len(parts) != 2 { + continue + } + + key := parts[0] + // Parse MongoDB configurations (format: MONGODB_[NAME]_[PROPERTY]) + if strings.HasPrefix(key, "MONGODB_") && strings.Contains(key, "_") { + segments := strings.Split(key, "_") + if len(segments) >= 3 { + dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_")) + // Skip if it's a standard MongoDB configuration + if dbName == "connection" || dbName == "dev" || dbName == "local" { + continue + } + + // Create or update MongoDB configuration + if _, exists := c.Databases[dbName]; !exists { + c.Databases[dbName] = DatabaseConfig{ + Name: dbName, + Type: "mongodb", + Host: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_HOST", "localhost"), + Port: getEnvAsInt("MONGODB_"+strings.ToUpper(dbName)+"_PORT", 27017), + Username: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_USER", ""), + Password: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_PASS", ""), + Database: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_DB", dbName), + SSLMode: getEnv("MONGOD_SSLMODE", "disable"), + MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100), + MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10), + ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")), + } + } + } + } + } +} + +func getEnvFromMap(config map[string]string, key, defaultValue string) string { + if value, exists := config[key]; exists { + return value + } + return defaultValue +} + +func getEnvAsIntFromMap(config map[string]string, key string, defaultValue int) int { + if value, exists := config[key]; exists { + if intValue, err := strconv.Atoi(value); err == nil { + return intValue + } + } + return defaultValue +} + +func parseDuration(durationStr string) time.Duration { + if duration, err := time.ParseDuration(durationStr); err == nil { + return duration + } + return 5 * time.Minute +} + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +func getEnvAsInt(key string, defaultValue int) int { + valueStr := getEnv(key, "") + if value, err := strconv.Atoi(valueStr); err == nil { + return value + } + return defaultValue +} + +func getEnvAsBool(key string, defaultValue bool) bool { + valueStr := getEnv(key, "") + if value, err := strconv.ParseBool(valueStr); err == nil { + return value + } + return defaultValue +} + +// parseSchemes parses comma-separated schemes string into a slice +func parseSchemes(schemesStr string) []string { + if schemesStr == "" { + return []string{"http"} + } + + schemes := strings.Split(schemesStr, ",") + for i, scheme := range schemes { + schemes[i] = strings.TrimSpace(scheme) + } + return schemes +} + +func (c *Config) Validate() error { + if len(c.Databases) == 0 { + log.Fatal("At least one database configuration is required") + } + + for name, db := range c.Databases { + if db.Host == "" { + log.Fatalf("Database host is required for %s", name) + } + if db.Username == "" { + log.Fatalf("Database username is required for %s", name) + } + if db.Password == "" { + log.Fatalf("Database password is required for %s", name) + } + if db.Database == "" { + log.Fatalf("Database name is required for %s", name) + } + } + + if c.Bpjs.BaseURL == "" { + log.Fatal("BPJS Base URL is required") + } + if c.Bpjs.ConsID == "" { + log.Fatal("BPJS Consumer ID is required") + } + if c.Bpjs.UserKey == "" { + log.Fatal("BPJS User Key is required") + } + if c.Bpjs.SecretKey == "" { + log.Fatal("BPJS Secret Key is required") + } + + // Validate Keycloak configuration if enabled + if c.Keycloak.Enabled { + if c.Keycloak.Issuer == "" { + log.Fatal("Keycloak issuer is required when Keycloak is enabled") + } + if c.Keycloak.Audience == "" { + log.Fatal("Keycloak audience is required when Keycloak is enabled") + } + if c.Keycloak.JwksURL == "" { + log.Fatal("Keycloak JWKS URL is required when Keycloak is enabled") + } + } + + // Validate SatuSehat configuration + if c.SatuSehat.OrgID == "" { + log.Fatal("SatuSehat Organization ID is required") + } + if c.SatuSehat.FasyakesID == "" { + log.Fatal("SatuSehat Fasyankes ID is required") + } + if c.SatuSehat.ClientID == "" { + log.Fatal("SatuSehat Client ID is required") + } + if c.SatuSehat.ClientSecret == "" { + log.Fatal("SatuSehat Client Secret is required") + } + if c.SatuSehat.AuthURL == "" { + log.Fatal("SatuSehat Auth URL is required") + } + if c.SatuSehat.BaseURL == "" { + log.Fatal("SatuSehat Base URL is required") + } + + return nil +} diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..b7f5b4f --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,699 @@ +package database + +import ( + "context" + "database/sql" + "fmt" + "log" // Import runtime package + + // Import debug package + "strconv" + "sync" + "time" + + "api-service/internal/config" + + _ "github.com/jackc/pgx/v5" // Import pgx driver + "github.com/lib/pq" + _ "gorm.io/driver/postgres" // Import GORM PostgreSQL driver + + _ "github.com/go-sql-driver/mysql" // MySQL driver for database/sql + _ "gorm.io/driver/mysql" // GORM MySQL driver + _ "gorm.io/driver/sqlserver" // GORM SQL Server driver + + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// DatabaseType represents supported database types +type DatabaseType string + +const ( + Postgres DatabaseType = "postgres" + MySQL DatabaseType = "mysql" + SQLServer DatabaseType = "sqlserver" + SQLite DatabaseType = "sqlite" + MongoDB DatabaseType = "mongodb" +) + +// Service represents a service that interacts with multiple databases +type Service interface { + Health() map[string]map[string]string + GetDB(name string) (*sql.DB, error) + GetMongoClient(name string) (*mongo.Client, error) + GetReadDB(name string) (*sql.DB, error) // For read replicas + Close() error + ListDBs() []string + GetDBType(name string) (DatabaseType, error) + // Tambahkan method untuk WebSocket notifications + ListenForChanges(ctx context.Context, dbName string, channels []string, callback func(string, string)) error + NotifyChange(dbName, channel, payload string) error + GetPrimaryDB(name string) (*sql.DB, error) // Helper untuk get primary DB +} + +type service struct { + sqlDatabases map[string]*sql.DB + mongoClients map[string]*mongo.Client + readReplicas map[string][]*sql.DB // Read replicas for load balancing + configs map[string]config.DatabaseConfig + readConfigs map[string][]config.DatabaseConfig + mu sync.RWMutex + readBalancer map[string]int // Round-robin counter for read replicas + listeners map[string]*pq.Listener // Tambahkan untuk tracking listeners + listenersMu sync.RWMutex +} + +var ( + dbManager *service + once sync.Once +) + +// New creates a new database service with multiple connections +func New(cfg *config.Config) Service { + once.Do(func() { + dbManager = &service{ + sqlDatabases: make(map[string]*sql.DB), + mongoClients: make(map[string]*mongo.Client), + readReplicas: make(map[string][]*sql.DB), + configs: make(map[string]config.DatabaseConfig), + readConfigs: make(map[string][]config.DatabaseConfig), + readBalancer: make(map[string]int), + listeners: make(map[string]*pq.Listener), + } + + log.Println("Initializing database service...") // Log when the initialization starts + // log.Printf("Current Goroutine ID: %d", runtime.NumGoroutine()) // Log the number of goroutines + // log.Printf("Stack Trace: %s", debug.Stack()) // Log the stack trace + dbManager.loadFromConfig(cfg) + + // Initialize all databases + for name, dbConfig := range dbManager.configs { + if err := dbManager.addDatabase(name, dbConfig); err != nil { + log.Printf("Failed to connect to database %s: %v", name, err) + } + } + + // Initialize read replicas + for name, replicaConfigs := range dbManager.readConfigs { + for i, replicaConfig := range replicaConfigs { + if err := dbManager.addReadReplica(name, i, replicaConfig); err != nil { + log.Printf("Failed to connect to read replica %s[%d]: %v", name, i, err) + } + } + } + }) + + return dbManager +} + +func (s *service) loadFromConfig(cfg *config.Config) { + s.mu.Lock() + defer s.mu.Unlock() + + // Load primary databases + for name, dbConfig := range cfg.Databases { + s.configs[name] = dbConfig + } + + // Load read replicas + for name, replicaConfigs := range cfg.ReadReplicas { + s.readConfigs[name] = replicaConfigs + } +} + +func (s *service) addDatabase(name string, config config.DatabaseConfig) error { + s.mu.Lock() + defer s.mu.Unlock() + + log.Printf("=== Database Connection Debug ===") + // log.Printf("Database: %s", name) + // log.Printf("Type: %s", config.Type) + // log.Printf("Host: %s", config.Host) + // log.Printf("Port: %d", config.Port) + // log.Printf("Database: %s", config.Database) + // log.Printf("Username: %s", config.Username) + // log.Printf("SSLMode: %s", config.SSLMode) + + var db *sql.DB + var err error + + dbType := DatabaseType(config.Type) + + switch dbType { + case Postgres: + db, err = s.openPostgresConnection(config) + case MySQL: + db, err = s.openMySQLConnection(config) + case SQLServer: + db, err = s.openSQLServerConnection(config) + case SQLite: + db, err = s.openSQLiteConnection(config) + case MongoDB: + return s.addMongoDB(name, config) + default: + return fmt.Errorf("unsupported database type: %s", config.Type) + } + + if err != nil { + log.Printf("โŒ Error connecting to database %s: %v", name, err) + log.Printf(" Database: %s@%s:%d/%s", config.Username, config.Host, config.Port, config.Database) + return err + } + + log.Printf("โœ… Successfully connected to database: %s", name) + return s.configureSQLDB(name, db, config.MaxOpenConns, config.MaxIdleConns, config.ConnMaxLifetime) +} + +func (s *service) addReadReplica(name string, index int, config config.DatabaseConfig) error { + s.mu.Lock() + defer s.mu.Unlock() + + var db *sql.DB + var err error + + dbType := DatabaseType(config.Type) + + switch dbType { + case Postgres: + db, err = s.openPostgresConnection(config) + case MySQL: + db, err = s.openMySQLConnection(config) + case SQLServer: + db, err = s.openSQLServerConnection(config) + case SQLite: + db, err = s.openSQLiteConnection(config) + default: + return fmt.Errorf("unsupported database type for read replica: %s", config.Type) + } + + if err != nil { + return err + } + + if s.readReplicas[name] == nil { + s.readReplicas[name] = make([]*sql.DB, 0) + } + + // Ensure we have enough slots + for len(s.readReplicas[name]) <= index { + s.readReplicas[name] = append(s.readReplicas[name], nil) + } + + s.readReplicas[name][index] = db + log.Printf("Successfully connected to read replica %s[%d]", name, index) + + return nil +} + +func (s *service) openPostgresConnection(config config.DatabaseConfig) (*sql.DB, error) { + connStr := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s", + config.Username, + config.Password, + config.Host, + config.Port, + config.Database, + config.SSLMode, + ) + + if config.Schema != "" { + connStr += "&search_path=" + config.Schema + } + + db, err := sql.Open("pgx", connStr) + if err != nil { + return nil, fmt.Errorf("failed to open PostgreSQL connection: %w", err) + } + + return db, nil +} + +func (s *service) openMySQLConnection(config config.DatabaseConfig) (*sql.DB, error) { + connStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true", + config.Username, + config.Password, + config.Host, + config.Port, + config.Database, + ) + + db, err := sql.Open("mysql", connStr) + if err != nil { + return nil, fmt.Errorf("failed to open MySQL connection: %w", err) + } + + return db, nil +} + +func (s *service) openSQLServerConnection(config config.DatabaseConfig) (*sql.DB, error) { + connStr := fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", + config.Username, + config.Password, + config.Host, + config.Port, + config.Database, + ) + + db, err := sql.Open("sqlserver", connStr) + if err != nil { + return nil, fmt.Errorf("failed to open SQL Server connection: %w", err) + } + + return db, nil +} + +func (s *service) openSQLiteConnection(config config.DatabaseConfig) (*sql.DB, error) { + dbPath := config.Path + if dbPath == "" { + dbPath = fmt.Sprintf("./data/%s.db", config.Database) + } + + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + return nil, fmt.Errorf("failed to open SQLite connection: %w", err) + } + + return db, nil +} + +func (s *service) addMongoDB(name string, config config.DatabaseConfig) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + uri := fmt.Sprintf("mongodb://%s:%s@%s:%d/%s", + config.Username, + config.Password, + config.Host, + config.Port, + config.Database, + ) + + client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri)) + if err != nil { + return fmt.Errorf("failed to connect to MongoDB: %w", err) + } + + s.mongoClients[name] = client + log.Printf("Successfully connected to MongoDB: %s", name) + + return nil +} + +func (s *service) configureSQLDB(name string, db *sql.DB, maxOpenConns, maxIdleConns int, connMaxLifetime time.Duration) error { + db.SetMaxOpenConns(maxOpenConns) + db.SetMaxIdleConns(maxIdleConns) + db.SetConnMaxLifetime(connMaxLifetime) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := db.PingContext(ctx); err != nil { + db.Close() + return fmt.Errorf("failed to ping database: %w", err) + } + + s.sqlDatabases[name] = db + log.Printf("Successfully connected to SQL database: %s", name) + + return nil +} + +// Health checks the health of all database connections by pinging each database. +func (s *service) Health() map[string]map[string]string { + s.mu.RLock() + defer s.mu.RUnlock() + + result := make(map[string]map[string]string) + + // Check SQL databases + for name, db := range s.sqlDatabases { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + stats := make(map[string]string) + + err := db.PingContext(ctx) + if err != nil { + stats["status"] = "down" + stats["error"] = fmt.Sprintf("db down: %v", err) + stats["type"] = "sql" + stats["role"] = "primary" + result[name] = stats + continue + } + + stats["status"] = "up" + stats["message"] = "It's healthy" + stats["type"] = "sql" + stats["role"] = "primary" + + dbStats := db.Stats() + stats["open_connections"] = strconv.Itoa(dbStats.OpenConnections) + stats["in_use"] = strconv.Itoa(dbStats.InUse) + stats["idle"] = strconv.Itoa(dbStats.Idle) + stats["wait_count"] = strconv.FormatInt(dbStats.WaitCount, 10) + stats["wait_duration"] = dbStats.WaitDuration.String() + stats["max_idle_closed"] = strconv.FormatInt(dbStats.MaxIdleClosed, 10) + stats["max_lifetime_closed"] = strconv.FormatInt(dbStats.MaxLifetimeClosed, 10) + + if dbStats.OpenConnections > 40 { + stats["message"] = "The database is experiencing heavy load." + } + + if dbStats.WaitCount > 1000 { + stats["message"] = "The database has a high number of wait events, indicating potential bottlenecks." + } + + if dbStats.MaxIdleClosed > int64(dbStats.OpenConnections)/2 { + stats["message"] = "Many idle connections are being closed, consider revising the connection pool settings." + } + + if dbStats.MaxLifetimeClosed > int64(dbStats.OpenConnections)/2 { + stats["message"] = "Many connections are being closed due to max lifetime, consider increasing max lifetime or revising the connection usage pattern." + } + + result[name] = stats + } + + // Check read replicas + for name, replicas := range s.readReplicas { + for i, db := range replicas { + if db == nil { + continue + } + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + replicaName := fmt.Sprintf("%s_replica_%d", name, i) + stats := make(map[string]string) + + err := db.PingContext(ctx) + if err != nil { + stats["status"] = "down" + stats["error"] = fmt.Sprintf("read replica down: %v", err) + stats["type"] = "sql" + stats["role"] = "replica" + result[replicaName] = stats + continue + } + + stats["status"] = "up" + stats["message"] = "Read replica healthy" + stats["type"] = "sql" + stats["role"] = "replica" + + dbStats := db.Stats() + stats["open_connections"] = strconv.Itoa(dbStats.OpenConnections) + stats["in_use"] = strconv.Itoa(dbStats.InUse) + stats["idle"] = strconv.Itoa(dbStats.Idle) + + result[replicaName] = stats + } + } + + // Check MongoDB connections + for name, client := range s.mongoClients { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + stats := make(map[string]string) + + err := client.Ping(ctx, nil) + if err != nil { + stats["status"] = "down" + stats["error"] = fmt.Sprintf("mongodb down: %v", err) + stats["type"] = "mongodb" + result[name] = stats + continue + } + + stats["status"] = "up" + stats["message"] = "It's healthy" + stats["type"] = "mongodb" + + result[name] = stats + } + + return result +} + +// GetDB returns a specific SQL database connection by name +func (s *service) GetDB(name string) (*sql.DB, error) { + log.Printf("Attempting to get database connection for: %s", name) + s.mu.RLock() + defer s.mu.RUnlock() + + db, exists := s.sqlDatabases[name] + if !exists { + log.Printf("Error: database %s not found", name) // Log the error + return nil, fmt.Errorf("database %s not found", name) + } + + log.Printf("Current connection pool state for %s: Open: %d, In Use: %d, Idle: %d", + name, db.Stats().OpenConnections, db.Stats().InUse, db.Stats().Idle) + s.mu.RLock() + defer s.mu.RUnlock() + + // db, exists := s.sqlDatabases[name] + // if !exists { + // log.Printf("Error: database %s not found", name) // Log the error + // return nil, fmt.Errorf("database %s not found", name) + // } + + return db, nil +} + +// GetReadDB returns a read replica connection using round-robin load balancing +func (s *service) GetReadDB(name string) (*sql.DB, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + replicas, exists := s.readReplicas[name] + if !exists || len(replicas) == 0 { + // Fallback to primary if no replicas available + return s.GetDB(name) + } + + // Round-robin load balancing + s.readBalancer[name] = (s.readBalancer[name] + 1) % len(replicas) + selected := replicas[s.readBalancer[name]] + + if selected == nil { + // Fallback to primary if replica is nil + return s.GetDB(name) + } + + return selected, nil +} + +// GetMongoClient returns a specific MongoDB client by name +func (s *service) GetMongoClient(name string) (*mongo.Client, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + client, exists := s.mongoClients[name] + if !exists { + return nil, fmt.Errorf("MongoDB client %s not found", name) + } + + return client, nil +} + +// ListDBs returns list of available database names +func (s *service) ListDBs() []string { + s.mu.RLock() + defer s.mu.RUnlock() + + names := make([]string, 0, len(s.sqlDatabases)+len(s.mongoClients)) + + for name := range s.sqlDatabases { + names = append(names, name) + } + + for name := range s.mongoClients { + names = append(names, name) + } + + return names +} + +// GetDBType returns the type of a specific database +func (s *service) GetDBType(name string) (DatabaseType, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + config, exists := s.configs[name] + if !exists { + return "", fmt.Errorf("database %s not found", name) + } + + return DatabaseType(config.Type), nil +} + +// Close closes all database connections +func (s *service) Close() error { + s.mu.Lock() + defer s.mu.Unlock() + + var errs []error + + for name, db := range s.sqlDatabases { + if err := db.Close(); err != nil { + errs = append(errs, fmt.Errorf("failed to close database %s: %w", name, err)) + } else { + log.Printf("Disconnected from SQL database: %s", name) + } + } + + for name, replicas := range s.readReplicas { + for i, db := range replicas { + if db != nil { + if err := db.Close(); err != nil { + errs = append(errs, fmt.Errorf("failed to close read replica %s[%d]: %w", name, i, err)) + } else { + log.Printf("Disconnected from read replica: %s[%d]", name, i) + } + } + } + } + + for name, client := range s.mongoClients { + if err := client.Disconnect(context.Background()); err != nil { + errs = append(errs, fmt.Errorf("failed to disconnect MongoDB client %s: %w", name, err)) + } else { + log.Printf("Disconnected from MongoDB: %s", name) + } + } + + s.sqlDatabases = make(map[string]*sql.DB) + s.mongoClients = make(map[string]*mongo.Client) + s.readReplicas = make(map[string][]*sql.DB) + s.configs = make(map[string]config.DatabaseConfig) + s.readConfigs = make(map[string][]config.DatabaseConfig) + + if len(errs) > 0 { + return fmt.Errorf("errors closing databases: %v", errs) + } + + return nil +} + +// GetPrimaryDB returns primary database connection +func (s *service) GetPrimaryDB(name string) (*sql.DB, error) { + return s.GetDB(name) +} + +// ListenForChanges implements PostgreSQL LISTEN/NOTIFY for real-time updates +func (s *service) ListenForChanges(ctx context.Context, dbName string, channels []string, callback func(string, string)) error { + s.mu.RLock() + config, exists := s.configs[dbName] + s.mu.RUnlock() + + if !exists { + return fmt.Errorf("database %s not found", dbName) + } + + // Only support PostgreSQL for LISTEN/NOTIFY + if DatabaseType(config.Type) != Postgres { + return fmt.Errorf("LISTEN/NOTIFY only supported for PostgreSQL databases") + } + + // Create connection string for listener + connStr := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s", + config.Username, + config.Password, + config.Host, + config.Port, + config.Database, + config.SSLMode, + ) + + // Create listener + listener := pq.NewListener( + connStr, + 10*time.Second, + time.Minute, + func(ev pq.ListenerEventType, err error) { + if err != nil { + log.Printf("Database listener (%s) error: %v", dbName, err) + } + }, + ) + + // Store listener for cleanup + s.listenersMu.Lock() + s.listeners[dbName] = listener + s.listenersMu.Unlock() + + // Listen to specified channels + for _, channel := range channels { + err := listener.Listen(channel) + if err != nil { + listener.Close() + return fmt.Errorf("failed to listen to channel %s: %w", channel, err) + } + log.Printf("Listening to database channel: %s on %s", channel, dbName) + } + + // Start listening loop + go func() { + defer func() { + listener.Close() + s.listenersMu.Lock() + delete(s.listeners, dbName) + s.listenersMu.Unlock() + log.Printf("Database listener for %s stopped", dbName) + }() + + for { + select { + case n := <-listener.Notify: + if n != nil { + callback(n.Channel, n.Extra) + } + case <-ctx.Done(): + return + case <-time.After(90 * time.Second): + // Send ping to keep connection alive + go func() { + if err := listener.Ping(); err != nil { + log.Printf("Listener ping failed for %s: %v", dbName, err) + } + }() + } + } + }() + + return nil +} + +// NotifyChange sends a notification to a PostgreSQL channel +func (s *service) NotifyChange(dbName, channel, payload string) error { + db, err := s.GetDB(dbName) + if err != nil { + return fmt.Errorf("failed to get database %s: %w", dbName, err) + } + + // Check if it's PostgreSQL + s.mu.RLock() + config, exists := s.configs[dbName] + s.mu.RUnlock() + + if !exists { + return fmt.Errorf("database %s configuration not found", dbName) + } + + if DatabaseType(config.Type) != Postgres { + return fmt.Errorf("NOTIFY only supported for PostgreSQL databases") + } + + // Execute NOTIFY + query := "SELECT pg_notify($1, $2)" + _, err = db.Exec(query, channel, payload) + if err != nil { + return fmt.Errorf("failed to send notification: %w", err) + } + + log.Printf("Sent notification to channel %s on %s: %s", channel, dbName, payload) + return nil +} diff --git a/internal/handlers/auth/auth.go b/internal/handlers/auth/auth.go new file mode 100644 index 0000000..3bd74dd --- /dev/null +++ b/internal/handlers/auth/auth.go @@ -0,0 +1,132 @@ +package handlers + +import ( + models "api-service/internal/models/auth" + services "api-service/internal/services/auth" + "net/http" + + "github.com/gin-gonic/gin" +) + +// AuthHandler handles authentication endpoints +type AuthHandler struct { + authService *services.AuthService +} + +// NewAuthHandler creates a new authentication handler +func NewAuthHandler(authService *services.AuthService) *AuthHandler { + return &AuthHandler{ + authService: authService, + } +} + +// Login godoc +// @Summary Login user and get JWT token +// @Description Authenticate user with username and password to receive JWT token +// @Tags Authentication +// @Accept json +// @Produce json +// @Param login body models.LoginRequest true "Login credentials" +// @Success 200 {object} models.TokenResponse +// @Failure 400 {object} map[string]string "Bad request" +// @Failure 401 {object} map[string]string "Unauthorized" +// @Router /api/v1/auth/login [post] +func (h *AuthHandler) Login(c *gin.Context) { + var loginReq models.LoginRequest + + // Bind JSON request + if err := c.ShouldBindJSON(&loginReq); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Authenticate user + tokenResponse, err := h.authService.Login(loginReq.Username, loginReq.Password) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, tokenResponse) +} + +// RefreshToken godoc +// @Summary Refresh JWT token +// @Description Refresh the JWT token using a valid refresh token +// @Tags Authentication +// @Accept json +// @Produce json +// @Param refresh body map[string]string true "Refresh token" +// @Success 200 {object} models.TokenResponse +// @Failure 400 {object} map[string]string "Bad request" +// @Failure 401 {object} map[string]string "Unauthorized" +// @Router /api/v1/auth/refresh [post] +func (h *AuthHandler) RefreshToken(c *gin.Context) { + // For now, this is a placeholder for refresh token functionality + // In a real implementation, you would handle refresh tokens here + c.JSON(http.StatusNotImplemented, gin.H{"error": "refresh token not implemented"}) +} + +// Register godoc +// @Summary Register new user +// @Description Register a new user account +// @Tags Authentication +// @Accept json +// @Produce json +// @Param register body map[string]string true "Registration data" +// @Success 201 {object} map[string]string +// @Failure 400 {object} map[string]string "Bad request" +// @Router /api/v1/auth/register [post] +func (h *AuthHandler) Register(c *gin.Context) { + var registerReq struct { + Username string `json:"username" binding:"required"` + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=6"` + Role string `json:"role" binding:"required"` + } + + if err := c.ShouldBindJSON(®isterReq); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + err := h.authService.RegisterUser( + registerReq.Username, + registerReq.Email, + registerReq.Password, + registerReq.Role, + ) + + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{"message": "user registered successfully"}) +} + +// Me godoc +// @Summary Get current user info +// @Description Get information about the currently authenticated user +// @Tags Authentication +// @Produce json +// @Security Bearer +// @Success 200 {object} models.User +// @Failure 401 {object} map[string]string "Unauthorized" +// @Router /api/v1/auth/me [get] +func (h *AuthHandler) Me(c *gin.Context) { + // Get user info from context (set by middleware) + userID, exists := c.Get("user_id") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"}) + return + } + + // In a real implementation, you would fetch user details from database + c.JSON(http.StatusOK, gin.H{ + "id": userID, + "username": c.GetString("username"), + "email": c.GetString("email"), + "role": c.GetString("role"), + }) +} diff --git a/internal/handlers/auth/token.go b/internal/handlers/auth/token.go new file mode 100644 index 0000000..02383c7 --- /dev/null +++ b/internal/handlers/auth/token.go @@ -0,0 +1,95 @@ +package handlers + +import ( + models "api-service/internal/models/auth" + services "api-service/internal/services/auth" + "net/http" + + "github.com/gin-gonic/gin" +) + +// TokenHandler handles token generation endpoints +type TokenHandler struct { + authService *services.AuthService +} + +// NewTokenHandler creates a new token handler +func NewTokenHandler(authService *services.AuthService) *TokenHandler { + return &TokenHandler{ + authService: authService, + } +} + +// GenerateToken godoc +// @Summary Generate JWT token +// @Description Generate a JWT token for a user +// @Tags Token +// @Accept json +// @Produce json +// @Param token body models.LoginRequest true "User credentials" +// @Success 200 {object} models.TokenResponse +// @Failure 400 {object} map[string]string "Bad request" +// @Failure 401 {object} map[string]string "Unauthorized" +// @Router /api/v1/token/generate [post] +func (h *TokenHandler) GenerateToken(c *gin.Context) { + var loginReq models.LoginRequest + + // Bind JSON request + if err := c.ShouldBindJSON(&loginReq); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Generate token + tokenResponse, err := h.authService.Login(loginReq.Username, loginReq.Password) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, tokenResponse) +} + +// GenerateTokenDirect godoc +// @Summary Generate token directly +// @Description Generate a JWT token directly without password verification (for testing) +// @Tags Token +// @Accept json +// @Produce json +// @Param user body map[string]string true "User info" +// @Success 200 {object} models.TokenResponse +// @Failure 400 {object} map[string]string "Bad request" +// @Router /api/v1/token/generate-direct [post] +func (h *TokenHandler) GenerateTokenDirect(c *gin.Context) { + var req struct { + Username string `json:"username" binding:"required"` + Email string `json:"email" binding:"required"` + Role string `json:"role" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Create a temporary user for token generation + user := &models.User{ + ID: "temp-" + req.Username, + Username: req.Username, + Email: req.Email, + Role: req.Role, + } + + // Generate token directly + token, err := h.authService.GenerateTokenForUser(user) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, models.TokenResponse{ + AccessToken: token, + TokenType: "Bearer", + ExpiresIn: 3600, + }) +} diff --git a/internal/handlers/healthcheck/healthcheck.go b/internal/handlers/healthcheck/healthcheck.go new file mode 100644 index 0000000..d109bff --- /dev/null +++ b/internal/handlers/healthcheck/healthcheck.go @@ -0,0 +1,24 @@ +package healthcheck + +import ( + "api-service/internal/database" + "net/http" + + "github.com/gin-gonic/gin" +) + +// HealthCheckHandler handles health check requests +type HealthCheckHandler struct { + dbService database.Service +} + +// NewHealthCheckHandler creates a new HealthCheckHandler +func NewHealthCheckHandler(dbService database.Service) *HealthCheckHandler { + return &HealthCheckHandler{dbService: dbService} +} + +// CheckHealth checks the health of the application +func (h *HealthCheckHandler) CheckHealth(c *gin.Context) { + healthStatus := h.dbService.Health() // Call the health check function from the database service + c.JSON(http.StatusOK, healthStatus) +} diff --git a/internal/handlers/retribusi/retribusi.go b/internal/handlers/retribusi/retribusi.go new file mode 100644 index 0000000..b5e9a94 --- /dev/null +++ b/internal/handlers/retribusi/retribusi.go @@ -0,0 +1,1401 @@ +package handlers + +import ( + "api-service/internal/config" + "api-service/internal/database" + models "api-service/internal/models" + "api-service/internal/models/retribusi" + utils "api-service/internal/utils/filters" + "api-service/internal/utils/validation" + "api-service/pkg/logger" + "context" + "database/sql" + "fmt" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "github.com/google/uuid" +) + +var ( + db database.Service + once sync.Once + validate *validator.Validate +) + +// Initialize the database connection and validator +func init() { + once.Do(func() { + db = database.New(config.LoadConfig()) + validate = validator.New() + + // Register custom validations if needed + validate.RegisterValidation("retribusi_status", validateRetribusiStatus) + + if db == nil { + logger.Fatal("Failed to initialize database connection") + } + }) +} + +// Custom validation for retribusi status +func validateRetribusiStatus(fl validator.FieldLevel) bool { + return models.IsValidStatus(fl.Field().String()) +} + +// RetribusiHandler handles retribusi services +type RetribusiHandler struct { + db database.Service +} + +// NewRetribusiHandler creates a new RetribusiHandler +func NewRetribusiHandler() *RetribusiHandler { + return &RetribusiHandler{ + db: db, + } +} + +// GetRetribusi godoc +// @Summary Get retribusi with pagination and optional aggregation +// @Description Returns a paginated list of retribusis with optional summary statistics +// @Tags Retribusi +// @Accept json +// @Produce json +// @Param limit query int false "Limit (max 100)" default(10) +// @Param offset query int false "Offset" default(0) +// @Param include_summary query bool false "Include aggregation summary" default(false) +// @Param status query string false "Filter by status" +// @Param jenis query string false "Filter by jenis" +// @Param dinas query string false "Filter by dinas" +// @Param search query string false "Search in multiple fields" +// @Success 200 {object} retribusi.RetribusiGetResponse "Success response" +// @Failure 400 {object} models.ErrorResponse "Bad request" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/retribusis [get] +func (h *RetribusiHandler) GetRetribusi(c *gin.Context) { + // Parse pagination parameters + limit, offset, err := h.parsePaginationParams(c) + if err != nil { + h.respondError(c, "Invalid pagination parameters", err, http.StatusBadRequest) + return + } + + // Parse filter parameters + filter := h.parseFilterParams(c) + includeAggregation := c.Query("include_summary") == "true" + + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + // Create context with timeout + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + // Execute concurrent operations + var ( + retribusis []retribusi.Retribusi + total int + aggregateData *models.AggregateData + wg sync.WaitGroup + errChan = make(chan error, 3) + mu sync.Mutex + ) + + // Fetch total count + wg.Add(1) + go func() { + defer wg.Done() + if err := h.getTotalCount(ctx, dbConn, filter, &total); err != nil { + mu.Lock() + errChan <- fmt.Errorf("failed to get total count: %w", err) + mu.Unlock() + } + }() + + // Fetch main data + wg.Add(1) + go func() { + defer wg.Done() + result, err := h.fetchRetribusis(ctx, dbConn, filter, limit, offset) + mu.Lock() + if err != nil { + errChan <- fmt.Errorf("failed to fetch data: %w", err) + } else { + retribusis = result + } + mu.Unlock() + }() + + // Fetch aggregation data if requested + if includeAggregation { + wg.Add(1) + go func() { + defer wg.Done() + result, err := h.getAggregateData(ctx, dbConn, filter) + mu.Lock() + if err != nil { + errChan <- fmt.Errorf("failed to get aggregate data: %w", err) + } else { + aggregateData = result + } + mu.Unlock() + }() + } + + // Wait for all goroutines + wg.Wait() + close(errChan) + + // Check for errors + for err := range errChan { + if err != nil { + h.logAndRespondError(c, "Data processing failed", err, http.StatusInternalServerError) + return + } + } + + // Build response + meta := h.calculateMeta(limit, offset, total) + response := retribusi.RetribusiGetResponse{ + Message: "Data retribusi berhasil diambil", + Data: retribusis, + Meta: meta, + } + + if includeAggregation && aggregateData != nil { + response.Summary = aggregateData + } + + c.JSON(http.StatusOK, response) +} + +// GetRetribusiByID godoc +// @Summary Get Retribusi by ID +// @Description Returns a single retribusi by ID +// @Tags Retribusi +// @Accept json +// @Produce json +// @Param id path string true "Retribusi ID (UUID)" +// @Success 200 {object} retribusi.RetribusiGetByIDResponse "Success response" +// @Failure 400 {object} models.ErrorResponse "Invalid ID format" +// @Failure 404 {object} models.ErrorResponse "Retribusi not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/retribusi/{id} [get] +func (h *RetribusiHandler) GetRetribusiByID(c *gin.Context) { + id := c.Param("id") + + // Validate UUID format + if _, err := uuid.Parse(id); err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + dataretribusi, err := h.getRetribusiByID(ctx, dbConn, id) + if err != nil { + if err == sql.ErrNoRows { + h.respondError(c, "Retribusi not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to get retribusi", err, http.StatusInternalServerError) + } + return + } + + response := retribusi.RetribusiGetByIDResponse{ + Message: "Retribusi details retrieved successfully", + Data: dataretribusi, + } + + c.JSON(http.StatusOK, response) +} + +// GetRetribusiDynamic godoc +// @Summary Get retribusi with dynamic filtering +// @Description Returns retribusis with advanced dynamic filtering like Directus +// @Tags Retribusi +// @Accept json +// @Produce json +// @Param fields query string false "Fields to select (e.g., fields=*.*)" +// @Param filter[column][operator] query string false "Dynamic filters (e.g., filter[Jenis][_eq]=value)" +// @Param sort query string false "Sort fields (e.g., sort=date_created,-Jenis)" +// @Param limit query int false "Limit" default(10) +// @Param offset query int false "Offset" default(0) +// @Success 200 {object} retribusi.RetribusiGetResponse "Success response" +// @Failure 400 {object} models.ErrorResponse "Bad request" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/retribusis/dynamic [get] +func (h *RetribusiHandler) GetRetribusiDynamic(c *gin.Context) { + // Parse query parameters + parser := utils.NewQueryParser().SetLimits(10, 100) + dynamicQuery, err := parser.ParseQuery(c.Request.URL.Query()) + if err != nil { + h.respondError(c, "Invalid query parameters", err, http.StatusBadRequest) + return + } + + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + // Create context with timeout + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + // Execute query with dynamic filtering + retribusis, total, err := h.fetchRetribusisDynamic(ctx, dbConn, dynamicQuery) + if err != nil { + h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError) + return + } + + // Build response + meta := h.calculateMeta(dynamicQuery.Limit, dynamicQuery.Offset, total) + response := retribusi.RetribusiGetResponse{ + Message: "Data retribusi berhasil diambil", + Data: retribusis, + Meta: meta, + } + + c.JSON(http.StatusOK, response) +} + +// fetchRetribusisDynamic executes dynamic query +func (h *RetribusiHandler) fetchRetribusisDynamic(ctx context.Context, dbConn *sql.DB, query utils.DynamicQuery) ([]retribusi.Retribusi, int, error) { + // Setup query builder + countBuilder := utils.NewQueryBuilder("data_retribusi"). + SetColumnMapping(map[string]string{ + "jenis": "Jenis", + "pelayanan": "Pelayanan", + "dinas": "Dinas", + "kelompok_obyek": "Kelompok_obyek", + "Kode_tarif": "Kode_tarif", + "kode_tarif": "Kode_tarif", + "tarif": "Tarif", + "satuan": "Satuan", + "tarif_overtime": "Tarif_overtime", + "satuan_overtime": "Satuan_overtime", + "rekening_pokok": "Rekening_pokok", + "rekening_denda": "Rekening_denda", + "uraian_1": "Uraian_1", + "uraian_2": "Uraian_2", + "uraian_3": "Uraian_3", + }). + SetAllowedColumns([]string{ + "id", "status", "sort", "user_created", "date_created", + "user_updated", "date_updated", "Jenis", "Pelayanan", + "Dinas", "Kelompok_obyek", "Kode_tarif", "Tarif", "Satuan", + "Tarif_overtime", "Satuan_overtime", "Rekening_pokok", + "Rekening_denda", "Uraian_1", "Uraian_2", "Uraian_3", + }) + + mainBuilder := utils.NewQueryBuilder("data_retribusi"). + SetColumnMapping(map[string]string{ + "jenis": "Jenis", + "pelayanan": "Pelayanan", + "dinas": "Dinas", + "kelompok_obyek": "Kelompok_obyek", + "Kode_tarif": "Kode_tarif", + "kode_tarif": "Kode_tarif", + "tarif": "Tarif", + "satuan": "Satuan", + "tarif_overtime": "Tarif_overtime", + "satuan_overtime": "Satuan_overtime", + "rekening_pokok": "Rekening_pokok", + "rekening_denda": "Rekening_denda", + "uraian_1": "Uraian_1", + "uraian_2": "Uraian_2", + "uraian_3": "Uraian_3", + }). + SetAllowedColumns([]string{ + "id", "status", "sort", "user_created", "date_created", + "user_updated", "date_updated", "Jenis", "Pelayanan", + "Dinas", "Kelompok_obyek", "Kode_tarif", "Tarif", "Satuan", + "Tarif_overtime", "Satuan_overtime", "Rekening_pokok", + "Rekening_denda", "Uraian_1", "Uraian_2", "Uraian_3", + }) + + // Add default filter to exclude deleted records + if len(query.Filters) > 0 { + query.Filters = append([]utils.FilterGroup{{ + Filters: []utils.DynamicFilter{{ + Column: "status", + Operator: utils.OpNotEqual, + Value: "deleted", + }}, + LogicOp: "AND", + }}, query.Filters...) + } else { + query.Filters = []utils.FilterGroup{{ + Filters: []utils.DynamicFilter{{ + Column: "status", + Operator: utils.OpNotEqual, + Value: "deleted", + }}, + LogicOp: "AND", + }} + } + + // Execute queries sequentially to avoid race conditions + var total int + var retribusis []retribusi.Retribusi + + // 1. Get total count first + countQuery := query + countQuery.Limit = 0 + countQuery.Offset = 0 + + countSQL, countArgs, err := countBuilder.BuildCountQuery(countQuery) + if err != nil { + return nil, 0, fmt.Errorf("failed to build count query: %w", err) + } + + if err := dbConn.QueryRowContext(ctx, countSQL, countArgs...).Scan(&total); err != nil { + return nil, 0, fmt.Errorf("failed to get total count: %w", err) + } + + // 2. Get main data + mainSQL, mainArgs, err := mainBuilder.BuildQuery(query) + if err != nil { + return nil, 0, fmt.Errorf("failed to build main query: %w", err) + } + + rows, err := dbConn.QueryContext(ctx, mainSQL, mainArgs...) + if err != nil { + return nil, 0, fmt.Errorf("failed to execute main query: %w", err) + } + defer rows.Close() + + for rows.Next() { + retribusi, err := h.scanRetribusi(rows) + if err != nil { + return nil, 0, fmt.Errorf("failed to scan retribusi: %w", err) + } + retribusis = append(retribusis, retribusi) + } + + if err := rows.Err(); err != nil { + return nil, 0, fmt.Errorf("rows iteration error: %w", err) + } + + return retribusis, total, nil +} + +// SearchRetribusiAdvanced provides advanced search capabilities +func (h *RetribusiHandler) SearchRetribusiAdvanced(c *gin.Context) { + // Parse complex search parameters + searchQuery := c.Query("q") + if searchQuery == "" { + // If no search query provided, return all records with default sorting + query := utils.DynamicQuery{ + Fields: []string{"*"}, + Filters: []utils.FilterGroup{}, // Empty filters - fetchRetribusisDynamic will add default deleted filter + Sort: []utils.SortField{{ + Column: "date_created", + Order: "DESC", + }}, + Limit: 20, + Offset: 0, + } + + // Parse pagination if provided + if limit := c.Query("limit"); limit != "" { + if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 100 { + query.Limit = l + } + } + + if offset := c.Query("offset"); offset != "" { + if o, err := strconv.Atoi(offset); err == nil && o >= 0 { + query.Offset = o + } + } + + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + // Execute query to get all records + retribusis, total, err := h.fetchRetribusisDynamic(ctx, dbConn, query) + if err != nil { + h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError) + return + } + + // Build response + meta := h.calculateMeta(query.Limit, query.Offset, total) + response := retribusi.RetribusiGetResponse{ + Message: "All records retrieved (no search query provided)", + Data: retribusis, + Meta: meta, + } + + c.JSON(http.StatusOK, response) + return + } + + // Build dynamic query for search + query := utils.DynamicQuery{ + Fields: []string{"*"}, + Filters: []utils.FilterGroup{{ + Filters: []utils.DynamicFilter{ + { + Column: "Jenis", + Operator: utils.OpContains, + Value: searchQuery, + LogicOp: "OR", + }, + { + Column: "Pelayanan", + Operator: utils.OpContains, + Value: searchQuery, + LogicOp: "OR", + }, + { + Column: "Dinas", + Operator: utils.OpContains, + Value: searchQuery, + LogicOp: "OR", + }, + { + Column: "Uraian_1", + Operator: utils.OpContains, + Value: searchQuery, + LogicOp: "OR", + }, + }, + LogicOp: "AND", + }}, + Sort: []utils.SortField{{ + Column: "date_created", + Order: "DESC", + }}, + Limit: 20, + Offset: 0, + } + + // Parse pagination if provided + if limit := c.Query("limit"); limit != "" { + if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 100 { + query.Limit = l + } + } + + if offset := c.Query("offset"); offset != "" { + if o, err := strconv.Atoi(offset); err == nil && o >= 0 { + query.Offset = o + } + } + + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + // Execute search + retribusis, total, err := h.fetchRetribusisDynamic(ctx, dbConn, query) + if err != nil { + h.logAndRespondError(c, "Search failed", err, http.StatusInternalServerError) + return + } + + // Build response + meta := h.calculateMeta(query.Limit, query.Offset, total) + response := retribusi.RetribusiGetResponse{ + Message: fmt.Sprintf("Search results for '%s'", searchQuery), + Data: retribusis, + Meta: meta, + } + + c.JSON(http.StatusOK, response) +} + +// CreateRetribusi godoc +// @Summary Create retribusi +// @Description Creates a new retribusi record +// @Tags Retribusi +// @Accept json +// @Produce json +// @Param request body retribusi.RetribusiCreateRequest true "Retribusi creation request" +// @Success 201 {object} retribusi.RetribusiCreateResponse "Retribusi created successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/retribusis [post] +func (h *RetribusiHandler) CreateRetribusi(c *gin.Context) { + var req retribusi.RetribusiCreateRequest + + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + + // Validate request + if err := validate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + // Validate duplicate and daily submission + if err := h.validateRetribusiSubmission(ctx, dbConn, &req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + dataretribusi, err := h.createRetribusi(ctx, dbConn, &req) + if err != nil { + h.logAndRespondError(c, "Failed to create retribusi", err, http.StatusInternalServerError) + return + } + + response := retribusi.RetribusiCreateResponse{ + Message: "Retribusi berhasil dibuat", + Data: dataretribusi, + } + + c.JSON(http.StatusCreated, response) +} + +// UpdateRetribusi godoc +// @Summary Update retribusi +// @Description Updates an existing retribusi record +// @Tags Retribusi +// @Accept json +// @Produce json +// @Param id path string true "Retribusi ID (UUID)" +// @Param request body retribusi.RetribusiUpdateRequest true "Retribusi update request" +// @Success 200 {object} retribusi.RetribusiUpdateResponse "Retribusi updated successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 404 {object} models.ErrorResponse "Retribusi not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/retribusi/{id} [put] +func (h *RetribusiHandler) UpdateRetribusi(c *gin.Context) { + id := c.Param("id") + + // Validate UUID format + if _, err := uuid.Parse(id); err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } + + var req retribusi.RetribusiUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + + // Set ID from path parameter + req.ID = id + + // Validate request + if err := validate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + dataretribusi, err := h.updateRetribusi(ctx, dbConn, &req) + if err != nil { + if err == sql.ErrNoRows { + h.respondError(c, "Retribusi not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to update retribusi", err, http.StatusInternalServerError) + } + return + } + + response := retribusi.RetribusiUpdateResponse{ + Message: "Retribusi berhasil diperbarui", + Data: dataretribusi, + } + + c.JSON(http.StatusOK, response) +} + +// DeleteRetribusi godoc +// @Summary Delete retribusi +// @Description Soft deletes a retribusi by setting status to 'deleted' +// @Tags Retribusi +// @Accept json +// @Produce json +// @Param id path string true "Retribusi ID (UUID)" +// @Success 200 {object} retribusi.RetribusiDeleteResponse "Retribusi deleted successfully" +// @Failure 400 {object} models.ErrorResponse "Invalid ID format" +// @Failure 404 {object} models.ErrorResponse "Retribusi not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/retribusi/{id} [delete] +func (h *RetribusiHandler) DeleteRetribusi(c *gin.Context) { + id := c.Param("id") + + // Validate UUID format + if _, err := uuid.Parse(id); err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + err = h.deleteRetribusi(ctx, dbConn, id) + if err != nil { + if err == sql.ErrNoRows { + h.respondError(c, "Retribusi not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to delete retribusi", err, http.StatusInternalServerError) + } + return + } + + response := retribusi.RetribusiDeleteResponse{ + Message: "Retribusi berhasil dihapus", + ID: id, + } + + c.JSON(http.StatusOK, response) +} + +// GetRetribusiStats godoc +// @Summary Get retribusi statistics +// @Description Returns comprehensive statistics about retribusi data +// @Tags Retribusi +// @Accept json +// @Produce json +// @Param status query string false "Filter statistics by status" +// @Success 200 {object} models.AggregateData "Statistics data" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/retribusis/stats [get] +func (h *RetribusiHandler) GetRetribusiStats(c *gin.Context) { + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + filter := h.parseFilterParams(c) + aggregateData, err := h.getAggregateData(ctx, dbConn, filter) + if err != nil { + h.logAndRespondError(c, "Failed to get statistics", err, http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Statistik retribusi berhasil diambil", + "data": aggregateData, + }) +} + +// Get retribusi by ID +func (h *RetribusiHandler) getRetribusiByID(ctx context.Context, dbConn *sql.DB, id string) (*retribusi.Retribusi, error) { + query := ` + SELECT + id, status, sort, user_created, date_created, user_updated, date_updated, + "Jenis", "Pelayanan", "Dinas", "Kelompok_obyek", "Kode_tarif", + "Tarif", "Satuan", "Tarif_overtime", "Satuan_overtime", + "Rekening_pokok", "Rekening_denda", "Uraian_1", "Uraian_2", "Uraian_3" + FROM data_retribusi + WHERE id = $1 AND status != 'deleted'` + + row := dbConn.QueryRowContext(ctx, query, id) + + var retribusi retribusi.Retribusi + err := row.Scan( + &retribusi.ID, &retribusi.Status, &retribusi.Sort, &retribusi.UserCreated, + &retribusi.DateCreated, &retribusi.UserUpdated, &retribusi.DateUpdated, + &retribusi.Jenis, &retribusi.Pelayanan, &retribusi.Dinas, &retribusi.KelompokObyek, + &retribusi.KodeTarif, &retribusi.Tarif, &retribusi.Satuan, &retribusi.TarifOvertime, + &retribusi.SatuanOvertime, &retribusi.RekeningPokok, &retribusi.RekeningDenda, + &retribusi.Uraian1, &retribusi.Uraian2, &retribusi.Uraian3, + ) + + if err != nil { + return nil, err + } + + return &retribusi, nil +} + +// Create retribusi +func (h *RetribusiHandler) createRetribusi(ctx context.Context, dbConn *sql.DB, req *retribusi.RetribusiCreateRequest) (*retribusi.Retribusi, error) { + id := uuid.New().String() + now := time.Now() + + query := ` + INSERT INTO data_retribusi ( + id, status, date_created, date_updated, + "Jenis", "Pelayanan", "Dinas", "Kelompok_obyek", "Kode_tarif", + "Tarif", "Satuan", "Tarif_overtime", "Satuan_overtime", + "Rekening_pokok", "Rekening_denda", "Uraian_1", "Uraian_2", "Uraian_3" + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) + RETURNING + id, status, sort, user_created, date_created, user_updated, date_updated, + "Jenis", "Pelayanan", "Dinas", "Kelompok_obyek", "Kode_tarif", + "Tarif", "Satuan", "Tarif_overtime", "Satuan_overtime", + "Rekening_pokok", "Rekening_denda", "Uraian_1", "Uraian_2", "Uraian_3"` + + row := dbConn.QueryRowContext(ctx, query, + id, req.Status, now, now, + req.Jenis, req.Pelayanan, req.Dinas, req.KelompokObyek, req.KodeTarif, + req.Tarif, req.Satuan, req.TarifOvertime, req.SatuanOvertime, + req.RekeningPokok, req.RekeningDenda, req.Uraian1, req.Uraian2, req.Uraian3, + ) + + var retribusi retribusi.Retribusi + err := row.Scan( + &retribusi.ID, &retribusi.Status, &retribusi.Sort, &retribusi.UserCreated, + &retribusi.DateCreated, &retribusi.UserUpdated, &retribusi.DateUpdated, + &retribusi.Jenis, &retribusi.Pelayanan, &retribusi.Dinas, &retribusi.KelompokObyek, + &retribusi.KodeTarif, &retribusi.Tarif, &retribusi.Satuan, &retribusi.TarifOvertime, + &retribusi.SatuanOvertime, &retribusi.RekeningPokok, &retribusi.RekeningDenda, + &retribusi.Uraian1, &retribusi.Uraian2, &retribusi.Uraian3, + ) + + if err != nil { + return nil, fmt.Errorf("failed to create retribusi: %w", err) + } + + return &retribusi, nil +} + +// Update retribusi +func (h *RetribusiHandler) updateRetribusi(ctx context.Context, dbConn *sql.DB, req *retribusi.RetribusiUpdateRequest) (*retribusi.Retribusi, error) { + now := time.Now() + + query := ` + UPDATE data_retribusi SET + status = $2, date_updated = $3, + "Jenis" = $4, "Pelayanan" = $5, "Dinas" = $6, "Kelompok_obyek" = $7, "Kode_tarif" = $8, + "Tarif" = $9, "Satuan" = $10, "Tarif_overtime" = $11, "Satuan_overtime" = $12, + "Rekening_pokok" = $13, "Rekening_denda" = $14, "Uraian_1" = $15, "Uraian_2" = $16, "Uraian_3" = $17 + WHERE id = $1 AND status != 'deleted' + RETURNING + id, status, sort, user_created, date_created, user_updated, date_updated, + "Jenis", "Pelayanan", "Dinas", "Kelompok_obyek", "Kode_tarif", + "Tarif", "Satuan", "Tarif_overtime", "Satuan_overtime", + "Rekening_pokok", "Rekening_denda", "Uraian_1", "Uraian_2", "Uraian_3"` + + row := dbConn.QueryRowContext(ctx, query, + req.ID, req.Status, now, + req.Jenis, req.Pelayanan, req.Dinas, req.KelompokObyek, req.KodeTarif, + req.Tarif, req.Satuan, req.TarifOvertime, req.SatuanOvertime, + req.RekeningPokok, req.RekeningDenda, req.Uraian1, req.Uraian2, req.Uraian3, + ) + + var retribusi retribusi.Retribusi + err := row.Scan( + &retribusi.ID, &retribusi.Status, &retribusi.Sort, &retribusi.UserCreated, + &retribusi.DateCreated, &retribusi.UserUpdated, &retribusi.DateUpdated, + &retribusi.Jenis, &retribusi.Pelayanan, &retribusi.Dinas, &retribusi.KelompokObyek, + &retribusi.KodeTarif, &retribusi.Tarif, &retribusi.Satuan, &retribusi.TarifOvertime, + &retribusi.SatuanOvertime, &retribusi.RekeningPokok, &retribusi.RekeningDenda, + &retribusi.Uraian1, &retribusi.Uraian2, &retribusi.Uraian3, + ) + + if err != nil { + return nil, fmt.Errorf("failed to update retribusi: %w", err) + } + + return &retribusi, nil +} + +// Soft delete retribusi +func (h *RetribusiHandler) deleteRetribusi(ctx context.Context, dbConn *sql.DB, id string) error { + now := time.Now() + + query := `UPDATE data_retribusi SET status = 'deleted', date_updated = $2 WHERE id = $1 AND status != 'deleted'` + + result, err := dbConn.ExecContext(ctx, query, id, now) + if err != nil { + return fmt.Errorf("failed to delete retribusi: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get affected rows: %w", err) + } + + if rowsAffected == 0 { + return sql.ErrNoRows + } + + return nil +} + +// Enhanced error handling +func (h *RetribusiHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) { + logger.Error(message, map[string]interface{}{ + "error": err.Error(), + "status_code": statusCode, + }) + h.respondError(c, message, err, statusCode) +} + +func (h *RetribusiHandler) respondError(c *gin.Context, message string, err error, statusCode int) { + errorMessage := message + if gin.Mode() == gin.ReleaseMode { + errorMessage = "Internal server error" + } + + c.JSON(statusCode, models.ErrorResponse{ + Error: errorMessage, + Code: statusCode, + Message: err.Error(), + Timestamp: time.Now(), + }) +} + +// Parse pagination parameters dengan validation yang lebih ketat +func (h *RetribusiHandler) parsePaginationParams(c *gin.Context) (int, int, error) { + limit := 10 // Default limit + offset := 0 // Default offset + + if limitStr := c.Query("limit"); limitStr != "" { + parsedLimit, err := strconv.Atoi(limitStr) + if err != nil { + return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr) + } + if parsedLimit <= 0 { + return 0, 0, fmt.Errorf("limit must be greater than 0") + } + if parsedLimit > 100 { + return 0, 0, fmt.Errorf("limit cannot exceed 100") + } + limit = parsedLimit + } + + if offsetStr := c.Query("offset"); offsetStr != "" { + parsedOffset, err := strconv.Atoi(offsetStr) + if err != nil { + return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr) + } + if parsedOffset < 0 { + return 0, 0, fmt.Errorf("offset cannot be negative") + } + offset = parsedOffset + } + + logger.Debug("Pagination parameters", map[string]interface{}{ + "limit": limit, + "offset": offset, + }) + return limit, offset, nil +} + +// Build WHERE clause dengan filter parameters +func (h *RetribusiHandler) buildWhereClause(filter retribusi.RetribusiFilter) (string, []interface{}) { + conditions := []string{"status != 'deleted'"} + args := []interface{}{} + paramCount := 1 + + if filter.Status != nil { + conditions = append(conditions, fmt.Sprintf("status = $%d", paramCount)) + args = append(args, *filter.Status) + paramCount++ + } + + if filter.Jenis != nil { + conditions = append(conditions, fmt.Sprintf(`"Jenis" ILIKE $%d`, paramCount)) + args = append(args, "%"+*filter.Jenis+"%") + paramCount++ + } + + if filter.Dinas != nil { + conditions = append(conditions, fmt.Sprintf(`"Dinas" ILIKE $%d`, paramCount)) + args = append(args, "%"+*filter.Dinas+"%") + paramCount++ + } + + if filter.KelompokObyek != nil { + conditions = append(conditions, fmt.Sprintf(`"Kelompok_obyek" ILIKE $%d`, paramCount)) + args = append(args, "%"+*filter.KelompokObyek+"%") + paramCount++ + } + + if filter.Search != nil { + searchCondition := fmt.Sprintf(`( + "Jenis" ILIKE $%d OR + "Pelayanan" ILIKE $%d OR + "Dinas" ILIKE $%d OR + "Kode_tarif" ILIKE $%d OR + "Uraian_1" ILIKE $%d OR + "Uraian_2" ILIKE $%d OR + "Uraian_3" ILIKE $%d + )`, paramCount, paramCount, paramCount, paramCount, paramCount, paramCount, paramCount) + conditions = append(conditions, searchCondition) + searchTerm := "%" + *filter.Search + "%" + args = append(args, searchTerm) + paramCount++ + } + + if filter.DateFrom != nil { + conditions = append(conditions, fmt.Sprintf("date_created >= $%d", paramCount)) + args = append(args, *filter.DateFrom) + paramCount++ + } + + if filter.DateTo != nil { + conditions = append(conditions, fmt.Sprintf("date_created <= $%d", paramCount)) + args = append(args, filter.DateTo.Add(24*time.Hour-time.Nanosecond)) // End of day + paramCount++ + } + + return strings.Join(conditions, " AND "), args +} + +// Optimized scanning function yang menggunakan sql.Null* types langsung +func (h *RetribusiHandler) scanRetribusi(rows *sql.Rows) (retribusi.Retribusi, error) { + var retribusi retribusi.Retribusi + + return retribusi, rows.Scan( + &retribusi.ID, + &retribusi.Status, + &retribusi.Sort, + &retribusi.UserCreated, + &retribusi.DateCreated, + &retribusi.UserUpdated, + &retribusi.DateUpdated, + &retribusi.Jenis, + &retribusi.Pelayanan, + &retribusi.Dinas, + &retribusi.KelompokObyek, + &retribusi.KodeTarif, + &retribusi.Tarif, + &retribusi.Satuan, + &retribusi.TarifOvertime, + &retribusi.SatuanOvertime, + &retribusi.RekeningPokok, + &retribusi.RekeningDenda, + &retribusi.Uraian1, + &retribusi.Uraian2, + &retribusi.Uraian3, + ) +} + +// Parse filter parameters dari query string +func (h *RetribusiHandler) parseFilterParams(c *gin.Context) retribusi.RetribusiFilter { + filter := retribusi.RetribusiFilter{} + + if status := c.Query("status"); status != "" { + if models.IsValidStatus(status) { + filter.Status = &status + } + } + + if jenis := c.Query("jenis"); jenis != "" { + filter.Jenis = &jenis + } + + if dinas := c.Query("dinas"); dinas != "" { + filter.Dinas = &dinas + } + + if kelompokObyek := c.Query("kelompok_obyek"); kelompokObyek != "" { + filter.KelompokObyek = &kelompokObyek + } + + if search := c.Query("search"); search != "" { + filter.Search = &search + } + + // Parse date filters + if dateFromStr := c.Query("date_from"); dateFromStr != "" { + if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil { + filter.DateFrom = &dateFrom + } + } + + if dateToStr := c.Query("date_to"); dateToStr != "" { + if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil { + filter.DateTo = &dateTo + } + } + + return filter +} + +// Get comprehensive aggregate data dengan filter support +func (h *RetribusiHandler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter retribusi.RetribusiFilter) (*models.AggregateData, error) { + aggregate := &models.AggregateData{ + ByStatus: make(map[string]int), + ByDinas: make(map[string]int), + ByJenis: make(map[string]int), + } + + // Build where clause untuk filter + whereClause, args := h.buildWhereClause(filter) + + // Use concurrent execution untuk performance + var wg sync.WaitGroup + var mu sync.Mutex + errChan := make(chan error, 4) + + // 1. Count by status + wg.Add(1) + go func() { + defer wg.Done() + statusQuery := fmt.Sprintf(` + SELECT status, COUNT(*) + FROM data_retribusi + WHERE %s + GROUP BY status + ORDER BY status`, whereClause) + + rows, err := dbConn.QueryContext(ctx, statusQuery, args...) + if err != nil { + errChan <- fmt.Errorf("status query failed: %w", err) + return + } + defer rows.Close() + + mu.Lock() + for rows.Next() { + var status string + var count int + if err := rows.Scan(&status, &count); err != nil { + mu.Unlock() + errChan <- fmt.Errorf("status scan failed: %w", err) + return + } + aggregate.ByStatus[status] = count + switch status { + case "active": + aggregate.TotalActive = count + case "draft": + aggregate.TotalDraft = count + case "inactive": + aggregate.TotalInactive = count + } + } + mu.Unlock() + + if err := rows.Err(); err != nil { + errChan <- fmt.Errorf("status iteration error: %w", err) + } + }() + + // 2. Count by Dinas + wg.Add(1) + go func() { + defer wg.Done() + dinasQuery := fmt.Sprintf(` + SELECT COALESCE("Dinas", 'Unknown') as dinas, COUNT(*) + FROM data_retribusi + WHERE %s AND "Dinas" IS NOT NULL AND TRIM("Dinas") != '' + GROUP BY "Dinas" + ORDER BY COUNT(*) DESC + LIMIT 10`, whereClause) + + rows, err := dbConn.QueryContext(ctx, dinasQuery, args...) + if err != nil { + errChan <- fmt.Errorf("dinas query failed: %w", err) + return + } + defer rows.Close() + + mu.Lock() + for rows.Next() { + var dinas string + var count int + if err := rows.Scan(&dinas, &count); err != nil { + mu.Unlock() + errChan <- fmt.Errorf("dinas scan failed: %w", err) + return + } + aggregate.ByDinas[dinas] = count + } + mu.Unlock() + + if err := rows.Err(); err != nil { + errChan <- fmt.Errorf("dinas iteration error: %w", err) + } + }() + + // 3. Count by Jenis + wg.Add(1) + go func() { + defer wg.Done() + jenisQuery := fmt.Sprintf(` + SELECT COALESCE("Jenis", 'Unknown') as jenis, COUNT(*) + FROM data_retribusi + WHERE %s AND "Jenis" IS NOT NULL AND TRIM("Jenis") != '' + GROUP BY "Jenis" + ORDER BY COUNT(*) DESC + LIMIT 10`, whereClause) + + rows, err := dbConn.QueryContext(ctx, jenisQuery, args...) + if err != nil { + errChan <- fmt.Errorf("jenis query failed: %w", err) + return + } + defer rows.Close() + + mu.Lock() + for rows.Next() { + var jenis string + var count int + if err := rows.Scan(&jenis, &count); err != nil { + mu.Unlock() + errChan <- fmt.Errorf("jenis scan failed: %w", err) + return + } + aggregate.ByJenis[jenis] = count + } + mu.Unlock() + + if err := rows.Err(); err != nil { + errChan <- fmt.Errorf("jenis iteration error: %w", err) + } + }() + + // 4. Get last updated time dan today statistics + wg.Add(1) + go func() { + defer wg.Done() + + // Last updated + lastUpdatedQuery := fmt.Sprintf(` + SELECT MAX(date_updated) + FROM data_retribusi + WHERE %s AND date_updated IS NOT NULL`, whereClause) + + var lastUpdated sql.NullTime + if err := dbConn.QueryRowContext(ctx, lastUpdatedQuery, args...).Scan(&lastUpdated); err != nil { + errChan <- fmt.Errorf("last updated query failed: %w", err) + return + } + + // Today statistics + today := time.Now().Format("2006-01-02") + todayStatsQuery := fmt.Sprintf(` + SELECT + SUM(CASE WHEN DATE(date_created) = $%d THEN 1 ELSE 0 END) as created_today, + SUM(CASE WHEN DATE(date_updated) = $%d AND DATE(date_created) != $%d THEN 1 ELSE 0 END) as updated_today + FROM data_retribusi + WHERE %s`, len(args)+1, len(args)+1, len(args)+1, whereClause) + + todayArgs := append(args, today) + var createdToday, updatedToday int + if err := dbConn.QueryRowContext(ctx, todayStatsQuery, todayArgs...).Scan(&createdToday, &updatedToday); err != nil { + errChan <- fmt.Errorf("today stats query failed: %w", err) + return + } + + mu.Lock() + if lastUpdated.Valid { + aggregate.LastUpdated = &lastUpdated.Time + } + aggregate.CreatedToday = createdToday + aggregate.UpdatedToday = updatedToday + mu.Unlock() + }() + + // Wait for all goroutines + wg.Wait() + close(errChan) + + // Check for errors + for err := range errChan { + if err != nil { + return nil, err + } + } + + return aggregate, nil +} + +// Get total count dengan filter support +func (h *RetribusiHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter retribusi.RetribusiFilter, total *int) error { + whereClause, args := h.buildWhereClause(filter) + countQuery := fmt.Sprintf(`SELECT COUNT(*) FROM data_retribusi WHERE %s`, whereClause) + + if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil { + return fmt.Errorf("total count query failed: %w", err) + } + + return nil +} + +// Enhanced fetchRetribusis dengan filter support +func (h *RetribusiHandler) fetchRetribusis(ctx context.Context, dbConn *sql.DB, filter retribusi.RetribusiFilter, limit, offset int) ([]retribusi.Retribusi, error) { + whereClause, args := h.buildWhereClause(filter) + + // Build the main query with pagination + query := fmt.Sprintf(` + SELECT + id, status, sort, user_created, date_created, user_updated, date_updated, + "Jenis", "Pelayanan", "Dinas", "Kelompok_obyek", "Kode_tarif", + "Tarif", "Satuan", "Tarif_overtime", "Satuan_overtime", + "Rekening_pokok", "Rekening_denda", "Uraian_1", "Uraian_2", "Uraian_3" + FROM data_retribusi + WHERE %s + ORDER BY date_created DESC NULLS LAST + LIMIT $%d OFFSET $%d`, + whereClause, len(args)+1, len(args)+2) + + // Add pagination parameters + args = append(args, limit, offset) + + rows, err := dbConn.QueryContext(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("fetch retribusis query failed: %w", err) + } + defer rows.Close() + + // Pre-allocate slice dengan kapasitas yang tepat + retribusis := make([]retribusi.Retribusi, 0, limit) + + for rows.Next() { + retribusi, err := h.scanRetribusi(rows) + if err != nil { + return nil, fmt.Errorf("scan retribusi failed: %w", err) + } + retribusis = append(retribusis, retribusi) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows iteration error: %w", err) + } + + logger.Info("Successfully fetched retribusis", map[string]interface{}{ + "count": len(retribusis), + "limit": limit, + "offset": offset, + }) + return retribusis, nil +} + +// Calculate pagination metadata +func (h *RetribusiHandler) calculateMeta(limit, offset, total int) models.MetaResponse { + totalPages := 0 + currentPage := 1 + + if limit > 0 { + totalPages = (total + limit - 1) / limit // Ceiling division + currentPage = (offset / limit) + 1 + } + + return models.MetaResponse{ + Limit: limit, + Offset: offset, + Total: total, + TotalPages: totalPages, + CurrentPage: currentPage, + HasNext: offset+limit < total, + HasPrev: offset > 0, + } +} + +// validateRetribusiSubmission performs validation for duplicate entries and daily submission limits +func (h *RetribusiHandler) validateRetribusiSubmission(ctx context.Context, dbConn *sql.DB, req *retribusi.RetribusiCreateRequest) error { + // Import the validation utility + validator := validation.NewDuplicateValidator(dbConn) + + // Use default retribusi configuration + config := validation.DefaultRetribusiConfig() + + // Validate duplicate entries with active status for today + err := validator.ValidateDuplicate(ctx, config, "dummy_id") + if err != nil { + return fmt.Errorf("validation failed: %w", err) + } + + // Validate once per day submission + err = validator.ValidateOncePerDay(ctx, "data_retribusi", "id", "date_created", "daily_limit") + if err != nil { + return fmt.Errorf("daily submission limit exceeded: %w", err) + } + + return nil +} + +// Example usage of the validation utility with custom configuration +func (h *RetribusiHandler) validateWithCustomConfig(ctx context.Context, dbConn *sql.DB, req *retribusi.RetribusiCreateRequest) error { + // Create validator instance + validator := validation.NewDuplicateValidator(dbConn) + + // Use custom configuration + config := validation.ValidationConfig{ + TableName: "data_retribusi", + IDColumn: "id", + StatusColumn: "status", + DateColumn: "date_created", + ActiveStatuses: []string{"active", "draft"}, + AdditionalFields: map[string]interface{}{ + "jenis": req.Jenis, + "dinas": req.Dinas, + }, + } + + // Validate with custom fields + fields := map[string]interface{}{ + "jenis": *req.Jenis, + "dinas": *req.Dinas, + } + + err := validator.ValidateDuplicateWithCustomFields(ctx, config, fields) + if err != nil { + return fmt.Errorf("custom validation failed: %w", err) + } + + return nil +} + +// GetLastSubmissionTime example +func (h *RetribusiHandler) getLastSubmissionTimeExample(ctx context.Context, dbConn *sql.DB, identifier string) (*time.Time, error) { + validator := validation.NewDuplicateValidator(dbConn) + return validator.GetLastSubmissionTime(ctx, "data_retribusi", "id", "date_created", identifier) +} diff --git a/internal/handlers/websocket/broadcast.go b/internal/handlers/websocket/broadcast.go new file mode 100644 index 0000000..abac19b --- /dev/null +++ b/internal/handlers/websocket/broadcast.go @@ -0,0 +1,111 @@ +package websocket + +import ( + "sync" + "time" +) + +// WebSocketBroadcaster defines the interface for broadcasting messages +type WebSocketBroadcaster interface { + BroadcastMessage(messageType string, data interface{}) +} + +// Broadcaster handles server-initiated broadcasts to WebSocket clients +type Broadcaster struct { + handler WebSocketBroadcaster + tickers []*time.Ticker + quit chan struct{} + mu sync.Mutex +} + +// NewBroadcaster creates a new Broadcaster instance +func NewBroadcaster(handler WebSocketBroadcaster) *Broadcaster { + return &Broadcaster{ + handler: handler, + tickers: make([]*time.Ticker, 0), + quit: make(chan struct{}), + } +} + +// StartHeartbeat starts sending periodic heartbeat messages to all clients +func (b *Broadcaster) StartHeartbeat(interval time.Duration) { + ticker := time.NewTicker(interval) + b.tickers = append(b.tickers, ticker) + go func() { + defer func() { + // Remove ticker from slice when done + for i, t := range b.tickers { + if t == ticker { + b.tickers = append(b.tickers[:i], b.tickers[i+1:]...) + break + } + } + }() + for { + select { + case <-ticker.C: + b.handler.BroadcastMessage("heartbeat", map[string]interface{}{ + "message": "Server heartbeat", + "timestamp": time.Now().Format(time.RFC3339), + }) + case <-b.quit: + ticker.Stop() + return + } + } + }() +} + +// Stop stops the broadcaster +func (b *Broadcaster) Stop() { + close(b.quit) + for _, ticker := range b.tickers { + if ticker != nil { + ticker.Stop() + } + } + b.tickers = nil +} + +// BroadcastNotification sends a notification message to all clients +func (b *Broadcaster) BroadcastNotification(title, message, level string) { + b.handler.BroadcastMessage("notification", map[string]interface{}{ + "title": title, + "message": message, + "level": level, + "time": time.Now().Format(time.RFC3339), + }) +} + +// SimulateDataStream simulates streaming data to clients (useful for demos) +func (b *Broadcaster) SimulateDataStream() { + ticker := time.NewTicker(100 * time.Millisecond) + b.tickers = append(b.tickers, ticker) + go func() { + defer func() { + // Remove ticker from slice when done + for i, t := range b.tickers { + if t == ticker { + b.tickers = append(b.tickers[:i], b.tickers[i+1:]...) + break + } + } + }() + counter := 0 + for { + select { + case <-ticker.C: + counter++ + b.handler.BroadcastMessage("data_stream", map[string]interface{}{ + "id": counter, + "value": counter * 10, + "timestamp": time.Now().Format(time.RFC3339), + "type": "simulated_data", + }) + case <-b.quit: + ticker.Stop() + return + } + } + }() +} diff --git a/internal/handlers/websocket/broadcast_test.go b/internal/handlers/websocket/broadcast_test.go new file mode 100644 index 0000000..4f129cf --- /dev/null +++ b/internal/handlers/websocket/broadcast_test.go @@ -0,0 +1,251 @@ +package websocket + +import ( + "sync" + "testing" + "time" +) + +// MockWebSocketHandler is a mock implementation for testing +type MockWebSocketHandler struct { + mu sync.Mutex + messages []map[string]interface{} + broadcasts []string +} + +func (m *MockWebSocketHandler) BroadcastMessage(messageType string, data interface{}) { + m.mu.Lock() + defer m.mu.Unlock() + m.broadcasts = append(m.broadcasts, messageType) + m.messages = append(m.messages, map[string]interface{}{ + "type": messageType, + "data": data, + }) +} + +func (m *MockWebSocketHandler) GetMessages() []map[string]interface{} { + m.mu.Lock() + defer m.mu.Unlock() + result := make([]map[string]interface{}, len(m.messages)) + copy(result, m.messages) + return result +} + +func (m *MockWebSocketHandler) GetBroadcasts() []string { + m.mu.Lock() + defer m.mu.Unlock() + result := make([]string, len(m.broadcasts)) + copy(result, m.broadcasts) + return result +} + +func (m *MockWebSocketHandler) Clear() { + m.mu.Lock() + defer m.mu.Unlock() + m.messages = make([]map[string]interface{}, 0) + m.broadcasts = make([]string, 0) +} + +func NewMockWebSocketHandler() *MockWebSocketHandler { + return &MockWebSocketHandler{ + messages: make([]map[string]interface{}, 0), + broadcasts: make([]string, 0), + } +} + +func TestBroadcaster_StartHeartbeat(t *testing.T) { + mockHandler := NewMockWebSocketHandler() + broadcaster := NewBroadcaster(mockHandler) + + // Start heartbeat with short interval for testing + broadcaster.StartHeartbeat(100 * time.Millisecond) + + // Wait for a few heartbeats + time.Sleep(350 * time.Millisecond) + + // Stop the broadcaster + broadcaster.Stop() + + // Check if heartbeats were sent + messages := mockHandler.GetMessages() + if len(messages) == 0 { + t.Error("Expected heartbeat messages, but got none") + } + + // Check that all messages are heartbeat type + broadcasts := mockHandler.GetBroadcasts() + for _, msgType := range broadcasts { + if msgType != "heartbeat" { + t.Errorf("Expected heartbeat message type, got %s", msgType) + } + } + + t.Logf("Received %d heartbeat messages", len(messages)) +} + +func TestBroadcaster_BroadcastNotification(t *testing.T) { + mockHandler := NewMockWebSocketHandler() + broadcaster := NewBroadcaster(mockHandler) + + // Send a notification + broadcaster.BroadcastNotification("Test Title", "Test Message", "info") + + // Check if notification was sent + messages := mockHandler.GetMessages() + if len(messages) != 1 { + t.Errorf("Expected 1 message, got %d", len(messages)) + return + } + + msg := messages[0] + if msg["type"] != "notification" { + t.Errorf("Expected message type 'notification', got %s", msg["type"]) + } + + data := msg["data"].(map[string]interface{}) + if data["title"] != "Test Title" { + t.Errorf("Expected title 'Test Title', got %s", data["title"]) + } + if data["message"] != "Test Message" { + t.Errorf("Expected message 'Test Message', got %s", data["message"]) + } + if data["level"] != "info" { + t.Errorf("Expected level 'info', got %s", data["level"]) + } + + t.Logf("Notification sent successfully: %+v", data) +} + +func TestBroadcaster_SimulateDataStream(t *testing.T) { + mockHandler := NewMockWebSocketHandler() + broadcaster := NewBroadcaster(mockHandler) + + // Start data stream with short interval for testing + broadcaster.SimulateDataStream() + + // Wait for a few data points + time.Sleep(550 * time.Millisecond) + + // Stop the broadcaster + broadcaster.Stop() + + // Check if data stream messages were sent + messages := mockHandler.GetMessages() + if len(messages) == 0 { + t.Error("Expected data stream messages, but got none") + } + + // Check that all messages are data_stream type + broadcasts := mockHandler.GetBroadcasts() + for _, msgType := range broadcasts { + if msgType != "data_stream" { + t.Errorf("Expected data_stream message type, got %s", msgType) + } + } + + // Check data structure + for i, msg := range messages { + data := msg["data"].(map[string]interface{}) + if data["type"] != "simulated_data" { + t.Errorf("Expected data type 'simulated_data', got %s", data["type"]) + } + if id, ok := data["id"].(int); ok { + if id != i+1 { + t.Errorf("Expected id %d, got %d", i+1, id) + } + } + if value, ok := data["value"].(int); ok { + expectedValue := (i + 1) * 10 + if value != expectedValue { + t.Errorf("Expected value %d, got %d", expectedValue, value) + } + } + } + + t.Logf("Received %d data stream messages", len(messages)) +} + +func TestBroadcaster_Stop(t *testing.T) { + mockHandler := NewMockWebSocketHandler() + broadcaster := NewBroadcaster(mockHandler) + + // Start heartbeat + broadcaster.StartHeartbeat(50 * time.Millisecond) + + // Wait a bit + time.Sleep(100 * time.Millisecond) + + // Stop the broadcaster + broadcaster.Stop() + + // Clear previous messages + mockHandler.Clear() + + // Wait a bit more to ensure no new messages are sent + time.Sleep(200 * time.Millisecond) + + // Check that no new messages were sent after stopping + messages := mockHandler.GetMessages() + if len(messages) > 0 { + t.Errorf("Expected no messages after stopping, but got %d", len(messages)) + } + + // Clear quit channel to allow reuse in tests + broadcaster.quit = make(chan struct{}) + + t.Log("Broadcaster stopped successfully") +} + +func TestBroadcaster_MultipleOperations(t *testing.T) { + mockHandler := NewMockWebSocketHandler() + broadcaster := NewBroadcaster(mockHandler) + + // Start heartbeat + broadcaster.StartHeartbeat(100 * time.Millisecond) + + // Send notification + broadcaster.BroadcastNotification("Test", "Message", "warning") + + // Start data stream + broadcaster.SimulateDataStream() + + // Wait for some activity + time.Sleep(350 * time.Millisecond) + + // Stop everything + broadcaster.Stop() + + // Check results + messages := mockHandler.GetMessages() + if len(messages) == 0 { + t.Error("Expected messages from multiple operations, but got none") + } + + broadcasts := mockHandler.GetBroadcasts() + hasHeartbeat := false + hasNotification := false + hasDataStream := false + + for _, msgType := range broadcasts { + switch msgType { + case "heartbeat": + hasHeartbeat = true + case "notification": + hasNotification = true + case "data_stream": + hasDataStream = true + } + } + + if !hasHeartbeat { + t.Error("Expected heartbeat messages") + } + if !hasNotification { + t.Error("Expected notification message") + } + if !hasDataStream { + t.Error("Expected data stream messages") + } + + t.Logf("Multiple operations test passed: %d total messages", len(messages)) +} diff --git a/internal/handlers/websocket/websocket.go b/internal/handlers/websocket/websocket.go new file mode 100644 index 0000000..1338019 --- /dev/null +++ b/internal/handlers/websocket/websocket.go @@ -0,0 +1,1621 @@ +package websocket + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "net" + "net/http" + "strings" + "sync" + "time" + + "api-service/internal/config" + "api-service/internal/database" + "api-service/pkg/logger" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/gorilla/websocket" +) + +const ( + // Timeout configurations (diperpanjang untuk stability) + ReadTimeout = 300 * time.Second // 5 menit (diperpanjang dari 60 detik) + WriteTimeout = 30 * time.Second // 30 detik untuk write operations + PingInterval = 60 * time.Second // 1 menit untuk ping (lebih konservatif) + PongTimeout = 70 * time.Second // 70 detik untuk menunggu pong response + + // Buffer sizes + ReadBufferSize = 8192 // Diperbesar untuk pesan besar + WriteBufferSize = 8192 // Diperbesar untuk pesan besar + ChannelBufferSize = 512 // Buffer untuk channel komunikasi + + // Connection limits + MaxMessageSize = 8192 // Maksimum ukuran pesan + HandshakeTimeout = 45 * time.Second +) + +type WebSocketMessage struct { + Type string `json:"type"` + Data interface{} `json:"data"` + Timestamp time.Time `json:"timestamp"` + ClientID string `json:"client_id,omitempty"` + MessageID string `json:"message_id,omitempty"` +} + +type Client struct { + ID string + StaticID string // Static ID for persistent identification + IPAddress string // Client IP address + Conn *websocket.Conn + Send chan WebSocketMessage + Hub *Hub + UserID string + Room string + ctx context.Context + cancel context.CancelFunc + lastPing time.Time + lastPong time.Time + connectedAt time.Time + mu sync.RWMutex + isActive bool +} + +type ClientInfo struct { + ID string `json:"id"` + StaticID string `json:"static_id"` + IPAddress string `json:"ip_address"` + UserID string `json:"user_id"` + Room string `json:"room"` + ConnectedAt time.Time `json:"connected_at"` + LastPing time.Time `json:"last_ping"` + LastPong time.Time `json:"last_pong"` + IsActive bool `json:"is_active"` +} +type DetailedStats struct { + ConnectedClients int `json:"connected_clients"` + UniqueIPs int `json:"unique_ips"` + StaticClients int `json:"static_clients"` + ActiveRooms int `json:"active_rooms"` + IPDistribution map[string]int `json:"ip_distribution"` + RoomDistribution map[string]int `json:"room_distribution"` + MessageQueueSize int `json:"message_queue_size"` + QueueWorkers int `json:"queue_workers"` + Uptime time.Duration `json:"uptime_seconds"` + Timestamp int64 `json:"timestamp"` +} + +type MonitoringData struct { + Stats DetailedStats `json:"stats"` + RecentActivity []ActivityLog `json:"recent_activity"` + SystemHealth map[string]interface{} `json:"system_health"` + Performance PerformanceMetrics `json:"performance"` +} + +type ActivityLog struct { + Timestamp time.Time `json:"timestamp"` + Event string `json:"event"` + ClientID string `json:"client_id"` + Details string `json:"details"` +} + +type PerformanceMetrics struct { + MessagesPerSecond float64 `json:"messages_per_second"` + AverageLatency float64 `json:"average_latency_ms"` + ErrorRate float64 `json:"error_rate_percent"` + MemoryUsage int64 `json:"memory_usage_bytes"` +} + +// Tambahkan field untuk monitoring di Hub +type Hub struct { + clients map[*Client]bool + clientsByID map[string]*Client // Track clients by ID + clientsByIP map[string][]*Client // Track clients by IP + clientsByStatic map[string]*Client // Track clients by static ID + broadcast chan WebSocketMessage + register chan *Client + unregister chan *Client + rooms map[string]map[*Client]bool + mu sync.RWMutex + ctx context.Context + cancel context.CancelFunc + dbService database.Service + messageQueue chan WebSocketMessage + queueWorkers int + + // Monitoring fields + startTime time.Time + messageCount int64 + errorCount int64 + activityLog []ActivityLog + activityMu sync.RWMutex +} + +type WebSocketHandler struct { + hub *Hub + logger *logger.Logger + upgrader websocket.Upgrader + config *config.Config + dbService database.Service + primaryDB string +} + +func NewWebSocketHandler(cfg *config.Config, dbService database.Service) *WebSocketHandler { + ctx, cancel := context.WithCancel(context.Background()) + + hub := &Hub{ + clients: make(map[*Client]bool), + clientsByID: make(map[string]*Client), + clientsByIP: make(map[string][]*Client), + clientsByStatic: make(map[string]*Client), + broadcast: make(chan WebSocketMessage, 1000), + register: make(chan *Client), + unregister: make(chan *Client), + rooms: make(map[string]map[*Client]bool), + ctx: ctx, + cancel: cancel, + dbService: dbService, + messageQueue: make(chan WebSocketMessage, 5000), + queueWorkers: 10, + startTime: time.Now(), + activityLog: make([]ActivityLog, 0, 1000), // Keep last 1000 activities + } + + handler := &WebSocketHandler{ + hub: hub, + logger: logger.Default(), + config: cfg, + dbService: dbService, + primaryDB: "default", + upgrader: websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, + ReadBufferSize: ReadBufferSize, // Gunakan konstanta + WriteBufferSize: WriteBufferSize, // Gunakan konstanta + HandshakeTimeout: HandshakeTimeout, // Gunakan konstanta + EnableCompression: true, + }, + } + + // Start hub and services + go hub.Run() + go handler.StartDatabaseListener() + // go handler.StartServerBroadcasters() + go handler.StartMessageQueue() + go handler.StartConnectionMonitor() + + return handler +} + +// Helper function to get client IP address +func getClientIP(c *gin.Context) string { + // Check for X-Forwarded-For header (proxy/load balancer) + xff := c.GetHeader("X-Forwarded-For") + if xff != "" { + ips := strings.Split(xff, ",") + if len(ips) > 0 { + return strings.TrimSpace(ips[0]) + } + } + + // Check for X-Real-IP header (nginx proxy) + xri := c.GetHeader("X-Real-IP") + if xri != "" { + return strings.TrimSpace(xri) + } + + // Get IP from RemoteAddr + ip, _, err := net.SplitHostPort(c.Request.RemoteAddr) + if err != nil { + return c.Request.RemoteAddr + } + + return ip +} + +// Generate static client ID based on IP and optional static ID +func generateClientID(ipAddress, staticID, userID string) string { + if staticID != "" { + // Use provided static ID + return staticID + } + + // Generate ID based on IP and userID + data := fmt.Sprintf("%s:%s:%d", ipAddress, userID, time.Now().Unix()/3600) // Hour-based for some uniqueness + hash := sha256.Sum256([]byte(data)) + return fmt.Sprintf("client_%s", hex.EncodeToString(hash[:8])) // Use first 8 bytes of hash +} + +// Generate IP-based static ID +func generateIPBasedID(ipAddress string) string { + hash := sha256.Sum256([]byte(ipAddress)) + return fmt.Sprintf("ip_%s", hex.EncodeToString(hash[:6])) // Use first 6 bytes of hash +} + +func (h *WebSocketHandler) HandleWebSocket(c *gin.Context) { + conn, err := h.upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + h.logger.Error(fmt.Sprintf("Failed to upgrade connection: %v", err)) + return + } + + // Get connection parameters + userID := c.Query("user_id") + if userID == "" { + userID = "anonymous" + } + + room := c.Query("room") + if room == "" { + room = "default" + } + + staticID := c.Query("static_id") // Optional static ID + useIPBasedID := c.Query("ip_based") // Use IP-based ID if "true" + + // Get client IP address + ipAddress := getClientIP(c) + + var clientID string + + // Determine client ID generation strategy + if useIPBasedID == "true" { + clientID = generateIPBasedID(ipAddress) + staticID = clientID + } else if staticID != "" { + clientID = staticID + } else { + clientID = generateClientID(ipAddress, staticID, userID) + } + + // Check if client with same static ID already exists + h.hub.mu.Lock() + if existingClient, exists := h.hub.clientsByStatic[clientID]; exists { + h.logger.Info(fmt.Sprintf("Disconnecting existing client %s for reconnection", clientID)) + // Disconnect existing client + existingClient.cancel() + existingClient.Conn.Close() + delete(h.hub.clientsByStatic, clientID) + } + h.hub.mu.Unlock() + + ctx, cancel := context.WithCancel(h.hub.ctx) + + client := &Client{ + ID: clientID, + StaticID: clientID, + IPAddress: ipAddress, + Conn: conn, + Send: make(chan WebSocketMessage, 256), + Hub: h.hub, + UserID: userID, + Room: room, + ctx: ctx, + cancel: cancel, + lastPing: time.Now(), + connectedAt: time.Now(), + } + + client.Hub.register <- client + + // Send welcome message with connection info + welcomeMsg := WebSocketMessage{ + Type: "welcome", + Data: map[string]interface{}{ + "message": "Connected to WebSocket server", + "client_id": client.ID, + "static_id": client.StaticID, + "ip_address": client.IPAddress, + "room": client.Room, + "user_id": client.UserID, + "connected_at": client.connectedAt.Unix(), + "id_type": h.getIDType(useIPBasedID, staticID), + }, + Timestamp: time.Now(), + MessageID: uuid.New().String(), + } + + select { + case client.Send <- welcomeMsg: + default: + close(client.Send) + cancel() + return + } + + go client.writePump() + go client.readPump() +} + +func (h *WebSocketHandler) getIDType(useIPBased, staticID string) string { + if useIPBased == "true" { + return "ip_based" + } else if staticID != "" { + return "static" + } + return "generated" +} + +func (h *Hub) Run() { + for { + select { + case client := <-h.register: + h.mu.Lock() + + // Register in main clients map + h.clients[client] = true + + // Register by ID + h.clientsByID[client.ID] = client + + // Register by static ID + if client.StaticID != "" { + h.clientsByStatic[client.StaticID] = client + } + + // Register by IP + if h.clientsByIP[client.IPAddress] == nil { + h.clientsByIP[client.IPAddress] = make([]*Client, 0) + } + h.clientsByIP[client.IPAddress] = append(h.clientsByIP[client.IPAddress], client) + + // Register in room + if client.Room != "" { + if h.rooms[client.Room] == nil { + h.rooms[client.Room] = make(map[*Client]bool) + } + h.rooms[client.Room][client] = true + } + + h.mu.Unlock() + + // Log activity + h.logActivity("client_connected", client.ID, + fmt.Sprintf("IP: %s, Static: %s, Room: %s", client.IPAddress, client.StaticID, client.Room)) + + logger.Info(fmt.Sprintf("Client %s (Static: %s, IP: %s) connected to room %s", + client.ID, client.StaticID, client.IPAddress, client.Room)) + + case client := <-h.unregister: + h.mu.Lock() + + if _, ok := h.clients[client]; ok { + // Remove from main clients + delete(h.clients, client) + close(client.Send) + + // Remove from clientsByID + delete(h.clientsByID, client.ID) + + // Remove from clientsByStatic + if client.StaticID != "" { + delete(h.clientsByStatic, client.StaticID) + } + + // Remove from clientsByIP + if ipClients, exists := h.clientsByIP[client.IPAddress]; exists { + for i, c := range ipClients { + if c == client { + h.clientsByIP[client.IPAddress] = append(ipClients[:i], ipClients[i+1:]...) + break + } + } + // If no more clients from this IP, remove the IP entry + if len(h.clientsByIP[client.IPAddress]) == 0 { + delete(h.clientsByIP, client.IPAddress) + } + } + + // Remove from room + if client.Room != "" { + if room, exists := h.rooms[client.Room]; exists { + delete(room, client) + if len(room) == 0 { + delete(h.rooms, client.Room) + } + } + } + } + + h.mu.Unlock() + + // Log activity + h.logActivity("client_disconnected", client.ID, + fmt.Sprintf("IP: %s, Duration: %v", client.IPAddress, time.Since(client.connectedAt))) + + client.cancel() + logger.Info(fmt.Sprintf("Client %s (IP: %s) disconnected", client.ID, client.IPAddress)) + + case message := <-h.broadcast: + h.messageCount++ + h.broadcastToClients(message) + + case <-h.ctx.Done(): + return + } + } +} + +// Enhanced message handling with client info +func (c *Client) handleMessage(msg WebSocketMessage) { + switch msg.Type { + case "ping": + // Respons ping dari client dengan informasi lebih lengkap + c.sendDirectResponse("pong", map[string]interface{}{ + "message": "Server is alive", + "timestamp": time.Now().Unix(), + "client_id": c.ID, + "static_id": c.StaticID, + "server_time": time.Now().Format(time.RFC3339), + "uptime": time.Since(c.connectedAt).Seconds(), + }) + + case "heartbeat": + // Tambahan: Handle heartbeat khusus + c.sendDirectResponse("heartbeat_ack", map[string]interface{}{ + "client_id": c.ID, + "timestamp": time.Now().Unix(), + "status": "alive", + }) + + case "connection_test": + // Tambahan: Test koneksi + c.sendDirectResponse("connection_test_result", map[string]interface{}{ + "latency_ms": 0, // Bisa dihitung jika perlu + "connection_id": c.ID, + "is_active": c.isClientActive(), + "last_ping": c.lastPing.Unix(), + "last_pong": c.lastPong.Unix(), + }) + + case "get_server_info": + c.Hub.mu.RLock() + connectedClients := len(c.Hub.clients) + roomsCount := len(c.Hub.rooms) + uniqueIPs := len(c.Hub.clientsByIP) + c.Hub.mu.RUnlock() + + c.sendDirectResponse("server_info", map[string]interface{}{ + "connected_clients": connectedClients, + "rooms_count": roomsCount, + "unique_ips": uniqueIPs, + "your_info": map[string]interface{}{ + "client_id": c.ID, + "static_id": c.StaticID, + "ip_address": c.IPAddress, + "user_id": c.UserID, + "room": c.Room, + "connected_at": c.connectedAt.Unix(), + }, + }) + + case "get_clients_by_ip": + c.handleGetClientsByIP(msg) + + case "get_client_info": + c.handleGetClientInfo(msg) + + case "direct_message": + c.handleDirectMessage(msg) + + case "room_message": + c.handleRoomMessage(msg) + + case "broadcast": + c.Hub.broadcast <- msg + c.sendDirectResponse("broadcast_sent", "Message broadcasted to all clients") + + case "get_online_users": + c.sendOnlineUsers() + + case "database_query": + c.handleDatabaseQuery(msg) + + default: + c.sendDirectResponse("message_received", fmt.Sprintf("Message received: %v", msg.Data)) + c.Hub.broadcast <- msg + } +} + +// Handle get clients by IP +func (c *Client) handleGetClientsByIP(msg WebSocketMessage) { + data, ok := msg.Data.(map[string]interface{}) + if !ok { + c.sendErrorResponse("Invalid request format", "Expected object with ip_address") + return + } + + targetIP, exists := data["ip_address"].(string) + if !exists { + targetIP = c.IPAddress // Default to current client's IP + } + + c.Hub.mu.RLock() + ipClients := c.Hub.clientsByIP[targetIP] + var clientInfos []ClientInfo + + for _, client := range ipClients { + clientInfos = append(clientInfos, ClientInfo{ + ID: client.ID, + StaticID: client.StaticID, + IPAddress: client.IPAddress, + UserID: client.UserID, + Room: client.Room, + ConnectedAt: client.connectedAt, + LastPing: client.lastPing, + }) + } + c.Hub.mu.RUnlock() + + c.sendDirectResponse("clients_by_ip", map[string]interface{}{ + "ip_address": targetIP, + "clients": clientInfos, + "count": len(clientInfos), + }) +} + +// Handle get specific client info +func (c *Client) handleGetClientInfo(msg WebSocketMessage) { + data, ok := msg.Data.(map[string]interface{}) + if !ok { + c.sendErrorResponse("Invalid request format", "Expected object with client_id or static_id") + return + } + + var targetClient *Client + + if clientID, exists := data["client_id"].(string); exists { + c.Hub.mu.RLock() + targetClient = c.Hub.clientsByID[clientID] + c.Hub.mu.RUnlock() + } else if staticID, exists := data["static_id"].(string); exists { + c.Hub.mu.RLock() + targetClient = c.Hub.clientsByStatic[staticID] + c.Hub.mu.RUnlock() + } + + if targetClient == nil { + c.sendErrorResponse("Client not found", "No client found with the specified ID") + return + } + + clientInfo := ClientInfo{ + ID: targetClient.ID, + StaticID: targetClient.StaticID, + IPAddress: targetClient.IPAddress, + UserID: targetClient.UserID, + Room: targetClient.Room, + ConnectedAt: targetClient.connectedAt, + LastPing: targetClient.lastPing, + } + + c.sendDirectResponse("client_info", clientInfo) +} + +// Enhanced online users with IP and static ID info +func (c *Client) sendOnlineUsers() { + c.Hub.mu.RLock() + var onlineUsers []map[string]interface{} + ipStats := make(map[string]int) + + for client := range c.Hub.clients { + onlineUsers = append(onlineUsers, map[string]interface{}{ + "client_id": client.ID, + "static_id": client.StaticID, + "user_id": client.UserID, + "room": client.Room, + "ip_address": client.IPAddress, + "connected_at": client.connectedAt.Unix(), + "last_ping": client.lastPing.Unix(), + }) + ipStats[client.IPAddress]++ + } + c.Hub.mu.RUnlock() + + c.sendDirectResponse("online_users", map[string]interface{}{ + "users": onlineUsers, + "total": len(onlineUsers), + "ip_stats": ipStats, + "unique_ips": len(ipStats), + }) +} + +// Enhanced database query handler +func (c *Client) handleDatabaseQuery(msg WebSocketMessage) { + data, ok := msg.Data.(map[string]interface{}) + if !ok { + c.sendErrorResponse("Invalid query format", "Expected object with query parameters") + return + } + + queryType, exists := data["query_type"].(string) + if !exists { + c.sendErrorResponse("Missing query_type", "query_type is required") + return + } + + switch queryType { + case "health_check": + health := c.Hub.dbService.Health() + c.sendDirectResponse("query_result", map[string]interface{}{ + "type": "health_check", + "result": health, + }) + + case "database_list": + dbList := c.Hub.dbService.ListDBs() + c.sendDirectResponse("query_result", map[string]interface{}{ + "type": "database_list", + "databases": dbList, + }) + + case "connection_stats": + c.Hub.mu.RLock() + stats := map[string]interface{}{ + "total_clients": len(c.Hub.clients), + "unique_ips": len(c.Hub.clientsByIP), + "static_clients": len(c.Hub.clientsByStatic), + "rooms": len(c.Hub.rooms), + } + c.Hub.mu.RUnlock() + + c.sendDirectResponse("query_result", map[string]interface{}{ + "type": "connection_stats", + "result": stats, + }) + + case "trigger_notification": + channel, channelExists := data["channel"].(string) + payload, payloadExists := data["payload"].(string) + + if !channelExists || !payloadExists { + c.sendErrorResponse("Missing Parameters", "channel and payload required") + return + } + + err := c.Hub.dbService.NotifyChange("default", channel, payload) + if err != nil { + c.sendErrorResponse("Notification Error", err.Error()) + return + } + + c.sendDirectResponse("query_result", map[string]interface{}{ + "type": "notification_sent", + "channel": channel, + "payload": payload, + }) + + default: + c.sendErrorResponse("Unsupported query", fmt.Sprintf("Query type '%s' not supported", queryType)) + } +} + +// Enhanced client search methods +func (h *WebSocketHandler) GetClientByID(clientID string) *Client { + h.hub.mu.RLock() + defer h.hub.mu.RUnlock() + return h.hub.clientsByID[clientID] +} + +func (h *WebSocketHandler) GetClientByStaticID(staticID string) *Client { + h.hub.mu.RLock() + defer h.hub.mu.RUnlock() + return h.hub.clientsByStatic[staticID] +} + +func (h *WebSocketHandler) GetClientsByIP(ipAddress string) []*Client { + h.hub.mu.RLock() + defer h.hub.mu.RUnlock() + return h.hub.clientsByIP[ipAddress] +} + +func (h *WebSocketHandler) SendToClientByStaticID(staticID string, messageType string, data interface{}) bool { + client := h.GetClientByStaticID(staticID) + if client == nil { + return false + } + + msg := WebSocketMessage{ + Type: messageType, + Data: data, + Timestamp: time.Now(), + ClientID: client.ID, + MessageID: uuid.New().String(), + } + + select { + case h.hub.messageQueue <- msg: + return true + default: + return false + } +} + +func (h *WebSocketHandler) BroadcastToIP(ipAddress string, messageType string, data interface{}) int { + clients := h.GetClientsByIP(ipAddress) + count := 0 + + for _, client := range clients { + msg := WebSocketMessage{ + Type: messageType, + Data: data, + Timestamp: time.Now(), + ClientID: client.ID, + MessageID: uuid.New().String(), + } + + select { + case h.hub.messageQueue <- msg: + count++ + default: + // Skip if queue is full + } + } + + return count +} + +// 1. SERVER BROADCAST DATA TANPA PERMINTAAN CLIENT +func (h *WebSocketHandler) StartServerBroadcasters() { + // Heartbeat broadcaster + go func() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + h.hub.mu.RLock() + connectedClients := len(h.hub.clients) + uniqueIPs := len(h.hub.clientsByIP) + staticClients := len(h.hub.clientsByStatic) + h.hub.mu.RUnlock() + + h.BroadcastMessage("server_heartbeat", map[string]interface{}{ + "message": "Server heartbeat", + "connected_clients": connectedClients, + "unique_ips": uniqueIPs, + "static_clients": staticClients, + "timestamp": time.Now().Unix(), + "server_id": "api-service-v1", + }) + case <-h.hub.ctx.Done(): + return + } + } + }() + + // System notification broadcaster + go func() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + dbHealth := h.dbService.Health() + h.BroadcastMessage("system_status", map[string]interface{}{ + "type": "system_notification", + "database": dbHealth, + "timestamp": time.Now().Unix(), + "uptime": time.Since(time.Now()).String(), + }) + case <-h.hub.ctx.Done(): + return + } + } + }() + + // Data stream broadcaster (demo purpose) + go func() { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + counter := 0 + + for { + select { + case <-ticker.C: + counter++ + h.BroadcastMessage("data_stream", map[string]interface{}{ + "id": counter, + "value": counter * 10, + "timestamp": time.Now().Unix(), + "type": "real_time_data", + }) + case <-h.hub.ctx.Done(): + return + } + } + }() +} + +// 4. SERVER MENGIRIM DATA JIKA ADA PERUBAHAN DATABASE +func (h *WebSocketHandler) StartDatabaseListener() { + // Cek apakah database utama adalah PostgreSQL + dbType, err := h.dbService.GetDBType(h.primaryDB) + if err != nil || dbType != database.Postgres { + h.logger.Error(fmt.Sprintf("Database notifications require PostgreSQL. Current DB type: %v", dbType)) + return + } + + channels := []string{"retribusi_changes", "peserta_changes", "system_changes"} + + err = h.dbService.ListenForChanges(h.hub.ctx, h.primaryDB, channels, func(channel, payload string) { + var changeData map[string]interface{} + if err := json.Unmarshal([]byte(payload), &changeData); err != nil { + h.logger.Error(fmt.Sprintf("Failed to parse database notification: %v", err)) + // Kirim raw payload jika JSON parsing gagal + changeData = map[string]interface{}{ + "raw_payload": payload, + "parse_error": err.Error(), + } + } + + h.BroadcastMessage("database_change", map[string]interface{}{ + "channel": channel, + "operation": changeData["operation"], + "table": changeData["table"], + "data": changeData["data"], + "timestamp": time.Now().Unix(), + "database": h.primaryDB, + }) + + h.logger.Info(fmt.Sprintf("Database change broadcasted: %s from %s", channel, h.primaryDB)) + }) + + if err != nil { + h.logger.Error(fmt.Sprintf("Failed to start database listener: %v", err)) + } +} + +func (h *WebSocketHandler) StartMessageQueue() { + // Start multiple workers for message processing + for i := 0; i < h.hub.queueWorkers; i++ { + go func(workerID int) { + for { + select { + case message := <-h.hub.messageQueue: + h.hub.broadcast <- message + case <-h.hub.ctx.Done(): + return + } + } + }(i) + } +} + +func (h *Hub) broadcastToClients(message WebSocketMessage) { + h.mu.RLock() + defer h.mu.RUnlock() + + if message.ClientID != "" { + // Send to specific client + for client := range h.clients { + if client.ID == message.ClientID { + select { + case client.Send <- message: + default: + h.unregisterClient(client) + } + break + } + } + return + } + + // Check if it's a room message + if data, ok := message.Data.(map[string]interface{}); ok { + if roomName, exists := data["room"].(string); exists { + if room, roomExists := h.rooms[roomName]; roomExists { + for client := range room { + select { + case client.Send <- message: + default: + h.unregisterClient(client) + } + } + } + return + } + } + + // Broadcast to all clients + for client := range h.clients { + select { + case client.Send <- message: + default: + h.unregisterClient(client) + } + } +} + +func (h *Hub) unregisterClient(client *Client) { + go func() { + h.unregister <- client + }() +} + +// 3. CLIENT MENGIRIM DATA KE CLIENT LAIN +func (c *Client) handleDirectMessage(msg WebSocketMessage) { + data, ok := msg.Data.(map[string]interface{}) + if !ok { + c.sendErrorResponse("Invalid direct message format", "Expected object with message data") + return + } + + targetClientID, exists := data["target_client_id"].(string) + if !exists { + c.sendErrorResponse("Missing target", "target_client_id is required") + return + } + + directMsg := WebSocketMessage{ + Type: "direct_message_received", + Data: map[string]interface{}{ + "from": c.ID, + "from_static_id": c.StaticID, + "from_ip": c.IPAddress, + "from_user_id": c.UserID, + "message": data["message"], + "original_msg_id": msg.MessageID, + }, + Timestamp: time.Now(), + ClientID: targetClientID, + MessageID: uuid.New().String(), + } + + c.Hub.broadcast <- directMsg + c.sendDirectResponse("direct_message_sent", map[string]interface{}{ + "target_client": targetClientID, + "message_id": directMsg.MessageID, + }) +} + +func (c *Client) handleRoomMessage(msg WebSocketMessage) { + data, ok := msg.Data.(map[string]interface{}) + if !ok { + c.sendErrorResponse("Invalid room message format", "Expected object with message data") + return + } + + roomName, exists := data["room"].(string) + if !exists { + roomName = c.Room + } + + roomMsg := WebSocketMessage{ + Type: "room_message_received", + Data: map[string]interface{}{ + "room": roomName, + "from": c.ID, + "from_static_id": c.StaticID, + "from_ip": c.IPAddress, + "from_user_id": c.UserID, + "message": data["message"], + "original_msg_id": msg.MessageID, + }, + Timestamp: time.Now(), + MessageID: uuid.New().String(), + } + + // Send to room members + c.Hub.mu.RLock() + if room, exists := c.Hub.rooms[roomName]; exists { + for client := range room { + if client.ID != c.ID { + select { + case client.Send <- roomMsg: + default: + logger.Error(fmt.Sprintf("Failed to send room message to client %s", client.ID)) + } + } + } + } + c.Hub.mu.RUnlock() + + c.sendDirectResponse("room_message_sent", map[string]interface{}{ + "room": roomName, + "message_id": roomMsg.MessageID, + }) +} + +func (c *Client) readPump() { + defer func() { + c.Hub.unregister <- c + c.Conn.Close() + logger.Info(fmt.Sprintf("Client %s readPump terminated", c.ID)) + }() + + // Konfigurasi connection limits + c.Conn.SetReadLimit(MaxMessageSize) + c.resetReadDeadline() // Set initial deadline + + // Ping/Pong handlers dengan logging yang lebih baik + c.Conn.SetPingHandler(func(message string) error { + logger.Debug(fmt.Sprintf("Client %s received ping", c.ID)) + c.resetReadDeadline() + return c.Conn.WriteControl(websocket.PongMessage, []byte(message), time.Now().Add(WriteTimeout)) + }) + + c.Conn.SetPongHandler(func(message string) error { + c.mu.Lock() + c.lastPong = time.Now() + c.isActive = true + c.mu.Unlock() + c.resetReadDeadline() + logger.Debug(fmt.Sprintf("Client %s received pong", c.ID)) + return nil + }) + + for { + select { + case <-c.ctx.Done(): + logger.Info(fmt.Sprintf("Client %s context cancelled", c.ID)) + return + default: + _, message, err := c.Conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, + websocket.CloseGoingAway, + websocket.CloseAbnormalClosure, + websocket.CloseNormalClosure) { + logger.Error(fmt.Sprintf("WebSocket unexpected close for client %s: %v", c.ID, err)) + } else { + logger.Info(fmt.Sprintf("WebSocket closed for client %s: %v", c.ID, err)) + } + return + } + + // PENTING: Reset deadline setiap kali ada pesan masuk + c.resetReadDeadline() + c.updateLastActivity() + + var msg WebSocketMessage + if err := json.Unmarshal(message, &msg); err != nil { + c.sendErrorResponse("Invalid message format", err.Error()) + continue + } + + msg.Timestamp = time.Now() + msg.ClientID = c.ID + if msg.MessageID == "" { + msg.MessageID = uuid.New().String() + } + + c.handleMessage(msg) + } + } +} + +// resetReadDeadline - Reset read deadline dengan timeout yang lebih panjang +func (c *Client) resetReadDeadline() { + c.Conn.SetReadDeadline(time.Now().Add(ReadTimeout)) +} + +// updateLastActivity - Update waktu aktivitas terakhir +func (c *Client) updateLastActivity() { + c.mu.Lock() + defer c.mu.Unlock() + c.lastPing = time.Now() + c.isActive = true +} + +// sendPing - Kirim ping message dengan proper error handling +func (c *Client) sendPing() error { + c.Conn.SetWriteDeadline(time.Now().Add(WriteTimeout)) + if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil { + return err + } + + c.mu.Lock() + c.lastPing = time.Now() + c.mu.Unlock() + + logger.Debug(fmt.Sprintf("Ping sent to client %s", c.ID)) + return nil +} + +// isPongTimeout - Cek apakah client sudah timeout dalam merespons pong +func (c *Client) isPongTimeout() bool { + c.mu.RLock() + defer c.mu.RUnlock() + + // Jika belum pernah menerima pong, gunakan lastPing sebagai baseline + lastActivity := c.lastPong + if lastActivity.IsZero() { + lastActivity = c.lastPing + } + + return time.Since(lastActivity) > PongTimeout +} + +// isClientActive - Cek apakah client masih aktif +func (c *Client) isClientActive() bool { + c.mu.RLock() + defer c.mu.RUnlock() + return c.isActive && time.Since(c.lastPing) < PongTimeout +} + +// gracefulClose - Tutup koneksi dengan graceful +func (c *Client) gracefulClose() { + c.mu.Lock() + c.isActive = false + c.mu.Unlock() + + // Kirim close message + c.Conn.SetWriteDeadline(time.Now().Add(WriteTimeout)) + c.Conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + + // Cancel context + c.cancel() + + logger.Info(fmt.Sprintf("Client %s closed gracefully", c.ID)) +} +func (c *Client) sendDirectResponse(messageType string, data interface{}) { + response := WebSocketMessage{ + Type: messageType, + Data: data, + Timestamp: time.Now(), + MessageID: uuid.New().String(), + } + + select { + case c.Send <- response: + default: + logger.Error("Failed to send direct response to client") + } +} + +func (c *Client) sendErrorResponse(error, details string) { + c.sendDirectResponse("error", map[string]interface{}{ + "error": error, + "details": details, + }) +} + +func (c *Client) writePump() { + ticker := time.NewTicker(PingInterval) + defer func() { + ticker.Stop() + c.Conn.Close() + logger.Info(fmt.Sprintf("Client %s writePump terminated", c.ID)) + }() + + for { + select { + case message, ok := <-c.Send: + c.Conn.SetWriteDeadline(time.Now().Add(WriteTimeout)) + if !ok { + c.Conn.WriteMessage(websocket.CloseMessage, []byte{}) + return + } + + if err := c.Conn.WriteJSON(message); err != nil { + logger.Error(fmt.Sprintf("Failed to write message to client %s: %v", c.ID, err)) + return + } + + case <-ticker.C: + // Kirim ping dan cek apakah client masih responsif + if err := c.sendPing(); err != nil { + logger.Error(fmt.Sprintf("Failed to send ping to client %s: %v", c.ID, err)) + return + } + + // Cek apakah client sudah terlalu lama tidak merespons pong + if c.isPongTimeout() { + logger.Warn(fmt.Sprintf("Client %s pong timeout, disconnecting", c.ID)) + return + } + + case <-c.ctx.Done(): + logger.Info(fmt.Sprintf("Client %s writePump context cancelled", c.ID)) + return + } + } +} + +// Broadcast methods +func (h *WebSocketHandler) BroadcastMessage(messageType string, data interface{}) { + msg := WebSocketMessage{ + Type: messageType, + Data: data, + Timestamp: time.Now(), + MessageID: uuid.New().String(), + } + + select { + case h.hub.messageQueue <- msg: + default: + logger.Error("Message queue full, dropping message") + } +} + +func (h *WebSocketHandler) BroadcastToRoom(room string, messageType string, data interface{}) { + msg := WebSocketMessage{ + Type: messageType, + Data: map[string]interface{}{ + "room": room, + "data": data, + }, + Timestamp: time.Now(), + MessageID: uuid.New().String(), + } + + select { + case h.hub.messageQueue <- msg: + default: + logger.Error("Message queue full, dropping room message") + } +} + +func (h *WebSocketHandler) SendToClient(clientID string, messageType string, data interface{}) { + msg := WebSocketMessage{ + Type: messageType, + Data: data, + Timestamp: time.Now(), + ClientID: clientID, + MessageID: uuid.New().String(), + } + + select { + case h.hub.messageQueue <- msg: + default: + logger.Error("Message queue full, dropping client message") + } +} + +func (h *WebSocketHandler) GetConnectedClients() int { + h.hub.mu.RLock() + defer h.hub.mu.RUnlock() + return len(h.hub.clients) +} + +func (h *WebSocketHandler) GetRoomClients(room string) int { + h.hub.mu.RLock() + defer h.hub.mu.RUnlock() + if roomClients, exists := h.hub.rooms[room]; exists { + return len(roomClients) + } + return 0 +} + +func (h *WebSocketHandler) Shutdown() { + h.hub.cancel() + + h.hub.mu.RLock() + for client := range h.hub.clients { + client.cancel() + } + h.hub.mu.RUnlock() +} + +// 1. GetDetailedStats - Mengembalikan statistik detail +func (h *WebSocketHandler) GetDetailedStats() DetailedStats { + h.hub.mu.RLock() + + // Calculate IP distribution + ipDistribution := make(map[string]int) + for ip, clients := range h.hub.clientsByIP { + ipDistribution[ip] = len(clients) + } + + // Calculate room distribution + roomDistribution := make(map[string]int) + for room, clients := range h.hub.rooms { + roomDistribution[room] = len(clients) + } + + stats := DetailedStats{ + ConnectedClients: len(h.hub.clients), + UniqueIPs: len(h.hub.clientsByIP), + StaticClients: len(h.hub.clientsByStatic), + ActiveRooms: len(h.hub.rooms), + IPDistribution: ipDistribution, + RoomDistribution: roomDistribution, + MessageQueueSize: len(h.hub.messageQueue), + QueueWorkers: h.hub.queueWorkers, + Uptime: time.Since(h.hub.startTime), + Timestamp: time.Now().Unix(), + } + + h.hub.mu.RUnlock() + + return stats +} + +// 2. GetAllClients - Mengembalikan semua client yang terhubung +func (h *WebSocketHandler) GetAllClients() []ClientInfo { + h.hub.mu.RLock() + defer h.hub.mu.RUnlock() + + var clients []ClientInfo + + for client := range h.hub.clients { + clientInfo := ClientInfo{ + ID: client.ID, + StaticID: client.StaticID, + IPAddress: client.IPAddress, + UserID: client.UserID, + Room: client.Room, + ConnectedAt: client.connectedAt, // Perbaikan: gunakan connectedAt bukan ConnectedAt + LastPing: client.lastPing, // Perbaikan: gunakan lastPing bukan LastPing + } + clients = append(clients, clientInfo) + } + + return clients +} + +// 3. GetAllRooms - Mengembalikan semua room dan anggotanya +func (h *WebSocketHandler) GetAllRooms() map[string][]ClientInfo { + h.hub.mu.RLock() + defer h.hub.mu.RUnlock() + + rooms := make(map[string][]ClientInfo) + + for roomName, clients := range h.hub.rooms { + var roomClients []ClientInfo + + for client := range clients { + clientInfo := ClientInfo{ + ID: client.ID, + StaticID: client.StaticID, + IPAddress: client.IPAddress, + UserID: client.UserID, + Room: client.Room, + ConnectedAt: client.connectedAt, + LastPing: client.lastPing, + } + roomClients = append(roomClients, clientInfo) + } + + rooms[roomName] = roomClients + } + + return rooms +} + +// 4. GetMonitoringData - Mengembalikan data monitoring lengkap +func (h *WebSocketHandler) GetMonitoringData() MonitoringData { + stats := h.GetDetailedStats() + + h.hub.activityMu.RLock() + recentActivity := make([]ActivityLog, 0) + // Get last 100 activities + start := len(h.hub.activityLog) - 100 + if start < 0 { + start = 0 + } + for i := start; i < len(h.hub.activityLog); i++ { + recentActivity = append(recentActivity, h.hub.activityLog[i]) + } + h.hub.activityMu.RUnlock() + + // Get system health from database service + systemHealth := make(map[string]interface{}) + if h.dbService != nil { + systemHealth["databases"] = h.dbService.Health() + systemHealth["available_dbs"] = h.dbService.ListDBs() + } + systemHealth["websocket_status"] = "healthy" + systemHealth["uptime_seconds"] = time.Since(h.hub.startTime).Seconds() + + // Calculate performance metrics + uptime := time.Since(h.hub.startTime) + var messagesPerSecond float64 + var errorRate float64 + + if uptime.Seconds() > 0 { + messagesPerSecond = float64(h.hub.messageCount) / uptime.Seconds() + } + + if h.hub.messageCount > 0 { + errorRate = (float64(h.hub.errorCount) / float64(h.hub.messageCount)) * 100 + } + + performance := PerformanceMetrics{ + MessagesPerSecond: messagesPerSecond, + AverageLatency: 2.5, // Mock value - implement actual latency tracking + ErrorRate: errorRate, + MemoryUsage: 0, // Mock value - implement actual memory tracking + } + + return MonitoringData{ + Stats: stats, + RecentActivity: recentActivity, + SystemHealth: systemHealth, + Performance: performance, + } +} +func (h *WebSocketHandler) GetRoomClientCount(room string) int { + h.hub.mu.RLock() + defer h.hub.mu.RUnlock() + + if roomClients, exists := h.hub.rooms[room]; exists { + return len(roomClients) + } + return 0 +} +func (h *WebSocketHandler) GetActiveClients(olderThan time.Duration) []ClientInfo { + h.hub.mu.RLock() + defer h.hub.mu.RUnlock() + + var activeClients []ClientInfo + cutoff := time.Now().Add(-olderThan) + + for client := range h.hub.clients { + if client.lastPing.After(cutoff) { + activeClients = append(activeClients, ClientInfo{ + ID: client.ID, + StaticID: client.StaticID, + IPAddress: client.IPAddress, + UserID: client.UserID, + Room: client.Room, + ConnectedAt: client.connectedAt, + LastPing: client.lastPing, + }) + } + } + + return activeClients +} +func (h *WebSocketHandler) CleanupInactiveClients(inactiveTimeout time.Duration) int { + h.hub.mu.RLock() + var inactiveClients []*Client + cutoff := time.Now().Add(-inactiveTimeout) + + for client := range h.hub.clients { + if client.lastPing.Before(cutoff) { + inactiveClients = append(inactiveClients, client) + } + } + h.hub.mu.RUnlock() + + // Disconnect inactive clients + for _, client := range inactiveClients { + h.hub.logActivity("cleanup_disconnect", client.ID, + fmt.Sprintf("Inactive for %v", time.Since(client.lastPing))) + client.cancel() + client.Conn.Close() + } + + return len(inactiveClients) +} + +// 5. DisconnectClient - Memutus koneksi client tertentu +func (h *WebSocketHandler) DisconnectClient(clientID string) bool { + h.hub.mu.RLock() + client, exists := h.hub.clientsByID[clientID] + h.hub.mu.RUnlock() + + if !exists { + return false + } + + // Log activity + h.hub.logActivity("force_disconnect", clientID, "Client disconnected by admin") + + // Cancel context and close connection + client.cancel() + client.Conn.Close() + + // The client will be automatically removed from hub in the Run() loop + return true +} + +func (h *WebSocketHandler) StartConnectionMonitor() { + ticker := time.NewTicker(2 * time.Minute) // Check setiap 2 menit + defer ticker.Stop() + + for { + select { + case <-ticker.C: + h.cleanupInactiveConnections() + h.logConnectionStats() + + case <-h.hub.ctx.Done(): + return + } + } +} + +// cleanupInactiveConnections - Bersihkan koneksi yang tidak aktif +func (h *WebSocketHandler) cleanupInactiveConnections() { + h.hub.mu.RLock() + var inactiveClients []*Client + + for client := range h.hub.clients { + if !client.isClientActive() { + inactiveClients = append(inactiveClients, client) + } + } + h.hub.mu.RUnlock() + + // Disconnect inactive clients + for _, client := range inactiveClients { + h.hub.logActivity("cleanup_disconnect", client.ID, + fmt.Sprintf("Inactive for %v", time.Since(client.lastPing))) + client.gracefulClose() + } + + if len(inactiveClients) > 0 { + logger.Info(fmt.Sprintf("Cleaned up %d inactive connections", len(inactiveClients))) + } +} + +// logConnectionStats - Log statistik koneksi +func (h *WebSocketHandler) logConnectionStats() { + stats := h.GetDetailedStats() + logger.Info(fmt.Sprintf("WebSocket Stats - Clients: %d, IPs: %d, Rooms: %d, Queue: %d", + stats.ConnectedClients, stats.UniqueIPs, stats.ActiveRooms, stats.MessageQueueSize)) +} + +func (h *Hub) logActivity(event, clientID, details string) { + h.activityMu.Lock() + defer h.activityMu.Unlock() + + activity := ActivityLog{ + Timestamp: time.Now(), + Event: event, + ClientID: clientID, + Details: details, + } + + h.activityLog = append(h.activityLog, activity) + + // Keep only last 1000 activities + if len(h.activityLog) > 1000 { + h.activityLog = h.activityLog[1:] + } +} + +// Example Database Use Triger +// -- Trigger function untuk notifikasi perubahan data +// CREATE OR REPLACE FUNCTION notify_data_change() RETURNS trigger AS $$ +// DECLARE +// channel text := 'retribusi_changes'; +// payload json; +// BEGIN +// -- Tentukan channel berdasarkan table +// IF TG_TABLE_NAME = 'retribusi' THEN +// channel := 'retribusi_changes'; +// ELSIF TG_TABLE_NAME = 'peserta' THEN +// channel := 'peserta_changes'; +// END IF; + +// -- Buat payload +// IF TG_OP = 'DELETE' THEN +// payload = json_build_object( +// 'operation', TG_OP, +// 'table', TG_TABLE_NAME, +// 'data', row_to_json(OLD) +// ); +// ELSE +// payload = json_build_object( +// 'operation', TG_OP, +// 'table', TG_TABLE_NAME, +// 'data', row_to_json(NEW) +// ); +// END IF; + +// -- Kirim notifikasi +// PERFORM pg_notify(channel, payload::text); + +// RETURN COALESCE(NEW, OLD); +// END; +// $$ LANGUAGE plpgsql; + +// -- Trigger untuk table retribusi +// CREATE TRIGGER retribusi_notify_trigger +// AFTER INSERT OR UPDATE OR DELETE ON retribusi +// FOR EACH ROW EXECUTE FUNCTION notify_data_change(); + +// -- Trigger untuk table peserta +// CREATE TRIGGER peserta_notify_trigger +// AFTER INSERT OR UPDATE OR DELETE ON peserta +// FOR EACH ROW EXECUTE FUNCTION notify_data_change(); diff --git a/internal/middleware/auth_middleware.go b/internal/middleware/auth_middleware.go new file mode 100644 index 0000000..1d3969c --- /dev/null +++ b/internal/middleware/auth_middleware.go @@ -0,0 +1,59 @@ +package middleware + +import ( + "fmt" + "net/http" + + "api-service/internal/config" + + "github.com/gin-gonic/gin" +) + +// ConfigurableAuthMiddleware provides flexible authentication based on configuration +func ConfigurableAuthMiddleware(cfg *config.Config) gin.HandlerFunc { + return func(c *gin.Context) { + // Skip authentication for development/testing if explicitly disabled + if !cfg.Keycloak.Enabled { + fmt.Println("Authentication is disabled - allowing all requests") + c.Next() + return + } + + // Use Keycloak authentication when enabled + AuthMiddleware()(c) + } +} + +// StrictAuthMiddleware enforces authentication regardless of Keycloak.Enabled setting +func StrictAuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + if appConfig == nil { + fmt.Println("AuthMiddleware: Config not initialized") + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "authentication service not configured"}) + return + } + + // Always enforce authentication + AuthMiddleware()(c) + } +} + +// OptionalKeycloakAuthMiddleware allows requests but adds authentication info if available +func OptionalKeycloakAuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + if appConfig == nil || !appConfig.Keycloak.Enabled { + c.Next() + return + } + + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + // No token provided, but continue + c.Next() + return + } + + // Try to validate token, but don't fail if invalid + AuthMiddleware()(c) + } +} diff --git a/internal/middleware/error_handler.go b/internal/middleware/error_handler.go new file mode 100644 index 0000000..7f6ab82 --- /dev/null +++ b/internal/middleware/error_handler.go @@ -0,0 +1,54 @@ +package middleware + +import ( + models "api-service/internal/models" + "net/http" + + "github.com/gin-gonic/gin" +) + +// ErrorHandler handles errors globally +func ErrorHandler() gin.HandlerFunc { + return func(c *gin.Context) { + c.Next() + + if len(c.Errors) > 0 { + err := c.Errors.Last() + status := http.StatusInternalServerError + + // Determine status code based on error type + switch err.Type { + case gin.ErrorTypeBind: + status = http.StatusBadRequest + case gin.ErrorTypeRender: + status = http.StatusUnprocessableEntity + case gin.ErrorTypePrivate: + status = http.StatusInternalServerError + } + + response := models.ErrorResponse{ + Error: "internal_error", + Message: err.Error(), + Code: status, + } + + c.JSON(status, response) + } + } +} + +// CORS middleware configuration +func CORSConfig() gin.HandlerFunc { + return gin.HandlerFunc(func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH") + c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + + c.Next() + }) +} diff --git a/internal/middleware/jwt_middleware.go b/internal/middleware/jwt_middleware.go new file mode 100644 index 0000000..708ef7f --- /dev/null +++ b/internal/middleware/jwt_middleware.go @@ -0,0 +1,77 @@ +package middleware + +import ( + services "api-service/internal/services/auth" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +// JWTAuthMiddleware validates JWT tokens generated by our auth service +func JWTAuthMiddleware(authService *services.AuthService) gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header missing"}) + return + } + + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format must be Bearer {token}"}) + return + } + + tokenString := parts[1] + + // Validate token + claims, err := authService.ValidateToken(tokenString) + if err != nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + return + } + + // Set user info in context + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("email", claims.Email) + c.Set("role", claims.Role) + + c.Next() + } +} + +// OptionalAuthMiddleware allows both authenticated and unauthenticated requests +func OptionalAuthMiddleware(authService *services.AuthService) gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + // No token provided, but continue + c.Next() + return + } + + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { + c.Next() + return + } + + tokenString := parts[1] + claims, err := authService.ValidateToken(tokenString) + if err != nil { + // Invalid token, but continue (don't abort) + c.Next() + return + } + + // Set user info in context + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("email", claims.Email) + c.Set("role", claims.Role) + + c.Next() + } +} diff --git a/internal/middleware/keycloak_middleware.go b/internal/middleware/keycloak_middleware.go new file mode 100644 index 0000000..a336154 --- /dev/null +++ b/internal/middleware/keycloak_middleware.go @@ -0,0 +1,254 @@ +package middleware + +/** Keycloak Auth Middleware **/ +import ( + "crypto/rsa" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "math/big" + "net/http" + "strings" + "sync" + "time" + + "api-service/internal/config" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + "golang.org/x/sync/singleflight" +) + +var ( + ErrInvalidToken = errors.New("invalid token") +) + +// JwksCache caches JWKS keys with expiration +type JwksCache struct { + mu sync.RWMutex + keys map[string]*rsa.PublicKey + expiresAt time.Time + sfGroup singleflight.Group + config *config.Config +} + +func NewJwksCache(cfg *config.Config) *JwksCache { + return &JwksCache{ + keys: make(map[string]*rsa.PublicKey), + config: cfg, + } +} + +func (c *JwksCache) GetKey(kid string) (*rsa.PublicKey, error) { + c.mu.RLock() + if key, ok := c.keys[kid]; ok && time.Now().Before(c.expiresAt) { + c.mu.RUnlock() + return key, nil + } + c.mu.RUnlock() + + // Fetch keys with singleflight to avoid concurrent fetches + v, err, _ := c.sfGroup.Do("fetch_jwks", func() (interface{}, error) { + return c.fetchKeys() + }) + if err != nil { + return nil, err + } + + keys := v.(map[string]*rsa.PublicKey) + + c.mu.Lock() + c.keys = keys + c.expiresAt = time.Now().Add(1 * time.Hour) // cache for 1 hour + c.mu.Unlock() + + key, ok := keys[kid] + if !ok { + return nil, fmt.Errorf("key with kid %s not found", kid) + } + return key, nil +} + +func (c *JwksCache) fetchKeys() (map[string]*rsa.PublicKey, error) { + if !c.config.Keycloak.Enabled { + return nil, fmt.Errorf("keycloak authentication is disabled") + } + + jwksURL := c.config.Keycloak.JwksURL + if jwksURL == "" { + // Construct JWKS URL from issuer if not explicitly provided + jwksURL = c.config.Keycloak.Issuer + "/protocol/openid-connect/certs" + } + + resp, err := http.Get(jwksURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var jwksData struct { + Keys []struct { + Kid string `json:"kid"` + Kty string `json:"kty"` + N string `json:"n"` + E string `json:"e"` + } `json:"keys"` + } + + if err := json.NewDecoder(resp.Body).Decode(&jwksData); err != nil { + return nil, err + } + + keys := make(map[string]*rsa.PublicKey) + for _, key := range jwksData.Keys { + if key.Kty != "RSA" { + continue + } + pubKey, err := parseRSAPublicKey(key.N, key.E) + if err != nil { + continue + } + keys[key.Kid] = pubKey + } + return keys, nil +} + +// parseRSAPublicKey parses RSA public key components from base64url strings +func parseRSAPublicKey(nStr, eStr string) (*rsa.PublicKey, error) { + nBytes, err := base64UrlDecode(nStr) + if err != nil { + return nil, err + } + eBytes, err := base64UrlDecode(eStr) + if err != nil { + return nil, err + } + + var eInt int + for _, b := range eBytes { + eInt = eInt<<8 + int(b) + } + + pubKey := &rsa.PublicKey{ + N: new(big.Int).SetBytes(nBytes), + E: eInt, + } + return pubKey, nil +} + +func base64UrlDecode(s string) ([]byte, error) { + // Add padding if missing + if m := len(s) % 4; m != 0 { + s += strings.Repeat("=", 4-m) + } + return base64.URLEncoding.DecodeString(s) +} + +// Global config instance +var appConfig *config.Config +var jwksCacheInstance *JwksCache + +// InitializeAuth initializes the auth middleware with config +func InitializeAuth(cfg *config.Config) { + appConfig = cfg + jwksCacheInstance = NewJwksCache(cfg) +} + +// AuthMiddleware validates Bearer token as Keycloak JWT token +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + if appConfig == nil { + fmt.Println("AuthMiddleware: Config not initialized") + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "authentication service not configured"}) + return + } + + if !appConfig.Keycloak.Enabled { + // Skip authentication if Keycloak is disabled but log for debugging + fmt.Println("AuthMiddleware: Keycloak authentication is disabled - allowing all requests") + c.Next() + return + } + + fmt.Println("AuthMiddleware: Checking Authorization header") // Debug log + + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + fmt.Println("AuthMiddleware: Authorization header missing") // Debug log + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header missing"}) + return + } + + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { + fmt.Println("AuthMiddleware: Invalid Authorization header format") // Debug log + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format must be Bearer {token}"}) + return + } + + tokenString := parts[1] + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + // Verify signing method + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + fmt.Printf("AuthMiddleware: Unexpected signing method: %v\n", token.Header["alg"]) // Debug log + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + kid, ok := token.Header["kid"].(string) + if !ok { + fmt.Println("AuthMiddleware: kid header not found") // Debug log + return nil, errors.New("kid header not found") + } + + return jwksCacheInstance.GetKey(kid) + }, jwt.WithIssuer(appConfig.Keycloak.Issuer), jwt.WithAudience(appConfig.Keycloak.Audience)) + + if err != nil || !token.Valid { + fmt.Printf("AuthMiddleware: Invalid or expired token: %v\n", err) // Debug log + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"}) + return + } + + fmt.Println("AuthMiddleware: Token valid, proceeding") // Debug log + // Token is valid, proceed + c.Next() + } +} + +/** JWT Bearer authentication middleware */ +// import ( +// "net/http" +// "strings" + +// "github.com/gin-gonic/gin" +// ) + +// AuthMiddleware validates Bearer token in Authorization header +func AuthJWTMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header missing"}) + return + } + + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format must be Bearer {token}"}) + return + } + + token := parts[1] + // For now, use a static token for validation. Replace with your logic. + const validToken = "your-static-token" + + if token != validToken { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) + return + } + + c.Next() + } +} diff --git a/internal/models/auth/auth.go b/internal/models/auth/auth.go new file mode 100644 index 0000000..872b45a --- /dev/null +++ b/internal/models/auth/auth.go @@ -0,0 +1,31 @@ +package models + +// LoginRequest represents the login request payload +type LoginRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// TokenResponse represents the token response +type TokenResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int64 `json:"expires_in"` +} + +// JWTClaims represents the JWT claims +type JWTClaims struct { + UserID string `json:"user_id"` + Username string `json:"username"` + Email string `json:"email"` + Role string `json:"role"` +} + +// User represents a user for authentication +type User struct { + ID string `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"-"` + Role string `json:"role"` +} diff --git a/internal/models/models.go b/internal/models/models.go new file mode 100644 index 0000000..2643ef8 --- /dev/null +++ b/internal/models/models.go @@ -0,0 +1,221 @@ +package models + +import ( + "database/sql" + "database/sql/driver" + "net/http" + "strconv" + "time" +) + +// NullableInt32 - your existing implementation +type NullableInt32 struct { + Int32 int32 `json:"int32,omitempty"` + Valid bool `json:"valid"` +} + +// Scan implements the sql.Scanner interface for NullableInt32 +func (n *NullableInt32) Scan(value interface{}) error { + var ni sql.NullInt32 + if err := ni.Scan(value); err != nil { + return err + } + n.Int32 = ni.Int32 + n.Valid = ni.Valid + return nil +} + +// Value implements the driver.Valuer interface for NullableInt32 +func (n NullableInt32) Value() (driver.Value, error) { + if !n.Valid { + return nil, nil + } + return n.Int32, nil +} + +// NullableString provides consistent nullable string handling +type NullableString struct { + String string `json:"string,omitempty"` + Valid bool `json:"valid"` +} + +// Scan implements the sql.Scanner interface for NullableString +func (n *NullableString) Scan(value interface{}) error { + var ns sql.NullString + if err := ns.Scan(value); err != nil { + return err + } + n.String = ns.String + n.Valid = ns.Valid + return nil +} + +// Value implements the driver.Valuer interface for NullableString +func (n NullableString) Value() (driver.Value, error) { + if !n.Valid { + return nil, nil + } + return n.String, nil +} + +// NullableTime provides consistent nullable time handling +type NullableTime struct { + Time time.Time `json:"time,omitempty"` + Valid bool `json:"valid"` +} + +// Scan implements the sql.Scanner interface for NullableTime +func (n *NullableTime) Scan(value interface{}) error { + var nt sql.NullTime + if err := nt.Scan(value); err != nil { + return err + } + n.Time = nt.Time + n.Valid = nt.Valid + return nil +} + +// Value implements the driver.Valuer interface for NullableTime +func (n NullableTime) Value() (driver.Value, error) { + if !n.Valid { + return nil, nil + } + return n.Time, nil +} + +// Metadata untuk pagination - dioptimalkan +type MetaResponse struct { + Limit int `json:"limit"` + Offset int `json:"offset"` + Total int `json:"total"` + TotalPages int `json:"total_pages"` + CurrentPage int `json:"current_page"` + HasNext bool `json:"has_next"` + HasPrev bool `json:"has_prev"` +} + +// Aggregate data untuk summary +type AggregateData struct { + TotalActive int `json:"total_active"` + TotalDraft int `json:"total_draft"` + TotalInactive int `json:"total_inactive"` + ByStatus map[string]int `json:"by_status"` + ByDinas map[string]int `json:"by_dinas,omitempty"` + ByJenis map[string]int `json:"by_jenis,omitempty"` + LastUpdated *time.Time `json:"last_updated,omitempty"` + CreatedToday int `json:"created_today"` + UpdatedToday int `json:"updated_today"` +} + +// Error response yang konsisten +type ErrorResponse struct { + Error string `json:"error"` + Code int `json:"code"` + Message string `json:"message"` + Timestamp time.Time `json:"timestamp"` +} + +// BaseRequest contains common fields for all BPJS requests +type BaseRequest struct { + RequestID string `json:"request_id,omitempty"` + Timestamp time.Time `json:"timestamp,omitempty"` +} + +// BaseResponse contains common response fields +type BaseResponse struct { + Status string `json:"status"` + Message string `json:"message,omitempty"` + RequestID string `json:"request_id,omitempty"` + Timestamp string `json:"timestamp,omitempty"` +} + +// ErrorResponse represents error response structure +type ErrorResponseBpjs struct { + Status string `json:"status"` + Message string `json:"message"` + RequestID string `json:"request_id,omitempty"` + Errors map[string]interface{} `json:"errors,omitempty"` + Code string `json:"code,omitempty"` +} + +// PaginationRequest contains pagination parameters +type PaginationRequest struct { + Page int `json:"page" validate:"min=1"` + Limit int `json:"limit" validate:"min=1,max=100"` + SortBy string `json:"sort_by,omitempty"` + SortDir string `json:"sort_dir,omitempty" validate:"omitempty,oneof=asc desc"` +} + +// PaginationResponse contains pagination metadata +type PaginationResponse struct { + CurrentPage int `json:"current_page"` + TotalPages int `json:"total_pages"` + TotalItems int64 `json:"total_items"` + ItemsPerPage int `json:"items_per_page"` + HasNext bool `json:"has_next"` + HasPrev bool `json:"has_previous"` +} + +// MetaInfo contains additional metadata +type MetaInfo struct { + Version string `json:"version"` + Environment string `json:"environment"` + ServerTime string `json:"server_time"` +} + +func GetStatusCodeFromMeta(metaCode interface{}) int { + statusCode := http.StatusOK + + if metaCode != nil { + switch v := metaCode.(type) { + case string: + if code, err := strconv.Atoi(v); err == nil { + if code >= 100 && code <= 599 { + statusCode = code + } else { + statusCode = http.StatusInternalServerError + } + } else { + statusCode = http.StatusInternalServerError + } + case int: + if v >= 100 && v <= 599 { + statusCode = v + } else { + statusCode = http.StatusInternalServerError + } + case float64: + code := int(v) + if code >= 100 && code <= 599 { + statusCode = code + } else { + statusCode = http.StatusInternalServerError + } + default: + statusCode = http.StatusInternalServerError + } + } + + return statusCode +} + +// Validation constants +const ( + StatusDraft = "draft" + StatusActive = "active" + StatusInactive = "inactive" + StatusDeleted = "deleted" +) + +// ValidStatuses untuk validasi +var ValidStatuses = []string{StatusDraft, StatusActive, StatusInactive} + +// IsValidStatus helper function +func IsValidStatus(status string) bool { + for _, validStatus := range ValidStatuses { + if status == validStatus { + return true + } + } + return false +} diff --git a/internal/models/retribusi/retribusi.go b/internal/models/retribusi/retribusi.go new file mode 100644 index 0000000..7907527 --- /dev/null +++ b/internal/models/retribusi/retribusi.go @@ -0,0 +1,228 @@ +package retribusi + +import ( + "api-service/internal/models" + "encoding/json" + "time" +) + +// Retribusi represents the data structure for the retribusi table +// with proper null handling and optimized JSON marshaling +type Retribusi struct { + ID string `json:"id" db:"id"` + Status string `json:"status" db:"status"` + Sort models.NullableInt32 `json:"sort,omitempty" db:"sort"` + UserCreated models.NullableString `json:"user_created,omitempty" db:"user_created"` + DateCreated models.NullableTime `json:"date_created,omitempty" db:"date_created"` + UserUpdated models.NullableString `json:"user_updated,omitempty" db:"user_updated"` + DateUpdated models.NullableTime `json:"date_updated,omitempty" db:"date_updated"` + Jenis models.NullableString `json:"jenis,omitempty" db:"Jenis"` + Pelayanan models.NullableString `json:"pelayanan,omitempty" db:"Pelayanan"` + Dinas models.NullableString `json:"dinas,omitempty" db:"Dinas"` + KelompokObyek models.NullableString `json:"kelompok_obyek,omitempty" db:"Kelompok_obyek"` + KodeTarif models.NullableString `json:"kode_tarif,omitempty" db:"Kode_tarif"` + Tarif models.NullableString `json:"tarif,omitempty" db:"Tarif"` + Satuan models.NullableString `json:"satuan,omitempty" db:"Satuan"` + TarifOvertime models.NullableString `json:"tarif_overtime,omitempty" db:"Tarif_overtime"` + SatuanOvertime models.NullableString `json:"satuan_overtime,omitempty" db:"Satuan_overtime"` + RekeningPokok models.NullableString `json:"rekening_pokok,omitempty" db:"Rekening_pokok"` + RekeningDenda models.NullableString `json:"rekening_denda,omitempty" db:"Rekening_denda"` + Uraian1 models.NullableString `json:"uraian_1,omitempty" db:"Uraian_1"` + Uraian2 models.NullableString `json:"uraian_2,omitempty" db:"Uraian_2"` + Uraian3 models.NullableString `json:"uraian_3,omitempty" db:"Uraian_3"` +} + +// Custom JSON marshaling untuk Retribusi agar NULL values tidak muncul di response +func (r Retribusi) MarshalJSON() ([]byte, error) { + type Alias Retribusi + aux := &struct { + Sort *int `json:"sort,omitempty"` + UserCreated *string `json:"user_created,omitempty"` + DateCreated *time.Time `json:"date_created,omitempty"` + UserUpdated *string `json:"user_updated,omitempty"` + DateUpdated *time.Time `json:"date_updated,omitempty"` + Jenis *string `json:"jenis,omitempty"` + Pelayanan *string `json:"pelayanan,omitempty"` + Dinas *string `json:"dinas,omitempty"` + KelompokObyek *string `json:"kelompok_obyek,omitempty"` + KodeTarif *string `json:"kode_tarif,omitempty"` + Tarif *string `json:"tarif,omitempty"` + Satuan *string `json:"satuan,omitempty"` + TarifOvertime *string `json:"tarif_overtime,omitempty"` + SatuanOvertime *string `json:"satuan_overtime,omitempty"` + RekeningPokok *string `json:"rekening_pokok,omitempty"` + RekeningDenda *string `json:"rekening_denda,omitempty"` + Uraian1 *string `json:"uraian_1,omitempty"` + Uraian2 *string `json:"uraian_2,omitempty"` + Uraian3 *string `json:"uraian_3,omitempty"` + *Alias + }{ + Alias: (*Alias)(&r), + } + + // Convert NullableInt32 to pointer + if r.Sort.Valid { + sort := int(r.Sort.Int32) + aux.Sort = &sort + } + if r.UserCreated.Valid { + aux.UserCreated = &r.UserCreated.String + } + if r.DateCreated.Valid { + aux.DateCreated = &r.DateCreated.Time + } + if r.UserUpdated.Valid { + aux.UserUpdated = &r.UserUpdated.String + } + if r.DateUpdated.Valid { + aux.DateUpdated = &r.DateUpdated.Time + } + if r.Jenis.Valid { + aux.Jenis = &r.Jenis.String + } + if r.Pelayanan.Valid { + aux.Pelayanan = &r.Pelayanan.String + } + if r.Dinas.Valid { + aux.Dinas = &r.Dinas.String + } + if r.KelompokObyek.Valid { + aux.KelompokObyek = &r.KelompokObyek.String + } + if r.KodeTarif.Valid { + aux.KodeTarif = &r.KodeTarif.String + } + if r.Tarif.Valid { + aux.Tarif = &r.Tarif.String + } + if r.Satuan.Valid { + aux.Satuan = &r.Satuan.String + } + if r.TarifOvertime.Valid { + aux.TarifOvertime = &r.TarifOvertime.String + } + if r.SatuanOvertime.Valid { + aux.SatuanOvertime = &r.SatuanOvertime.String + } + if r.RekeningPokok.Valid { + aux.RekeningPokok = &r.RekeningPokok.String + } + if r.RekeningDenda.Valid { + aux.RekeningDenda = &r.RekeningDenda.String + } + if r.Uraian1.Valid { + aux.Uraian1 = &r.Uraian1.String + } + if r.Uraian2.Valid { + aux.Uraian2 = &r.Uraian2.String + } + if r.Uraian3.Valid { + aux.Uraian3 = &r.Uraian3.String + } + + return json.Marshal(aux) +} + +// Helper methods untuk mendapatkan nilai yang aman +func (r *Retribusi) GetJenis() string { + if r.Jenis.Valid { + return r.Jenis.String + } + return "" +} + +func (r *Retribusi) GetDinas() string { + if r.Dinas.Valid { + return r.Dinas.String + } + return "" +} + +func (r *Retribusi) GetTarif() string { + if r.Tarif.Valid { + return r.Tarif.String + } + return "" +} + +// Response struct untuk GET by ID - diperbaiki struktur +type RetribusiGetByIDResponse struct { + Message string `json:"message"` + Data *Retribusi `json:"data"` +} + +// Request struct untuk create - dioptimalkan dengan validasi +type RetribusiCreateRequest struct { + Status string `json:"status" validate:"required,oneof=draft active inactive"` + Jenis *string `json:"jenis,omitempty" validate:"omitempty,min=1,max=255"` + Pelayanan *string `json:"pelayanan,omitempty" validate:"omitempty,min=1,max=255"` + Dinas *string `json:"dinas,omitempty" validate:"omitempty,min=1,max=255"` + KelompokObyek *string `json:"kelompok_obyek,omitempty" validate:"omitempty,min=1,max=255"` + KodeTarif *string `json:"kode_tarif,omitempty" validate:"omitempty,min=1,max=255"` + Uraian1 *string `json:"uraian_1,omitempty"` + Uraian2 *string `json:"uraian_2,omitempty"` + Uraian3 *string `json:"uraian_3,omitempty"` + Tarif *string `json:"tarif,omitempty" validate:"omitempty,numeric"` + Satuan *string `json:"satuan,omitempty" validate:"omitempty,min=1,max=255"` + TarifOvertime *string `json:"tarif_overtime,omitempty" validate:"omitempty,numeric"` + SatuanOvertime *string `json:"satuan_overtime,omitempty" validate:"omitempty,min=1,max=255"` + RekeningPokok *string `json:"rekening_pokok,omitempty" validate:"omitempty,min=1,max=255"` + RekeningDenda *string `json:"rekening_denda,omitempty" validate:"omitempty,min=1,max=255"` +} + +// Response struct untuk create +type RetribusiCreateResponse struct { + Message string `json:"message"` + Data *Retribusi `json:"data"` +} + +// Update request - sama seperti create tapi dengan ID +type RetribusiUpdateRequest struct { + ID string `json:"-" validate:"required,uuid4"` // ID dari URL path + Status string `json:"status" validate:"required,oneof=draft active inactive"` + Jenis *string `json:"jenis,omitempty" validate:"omitempty,min=1,max=255"` + Pelayanan *string `json:"pelayanan,omitempty" validate:"omitempty,min=1,max=255"` + Dinas *string `json:"dinas,omitempty" validate:"omitempty,min=1,max=255"` + KelompokObyek *string `json:"kelompok_obyek,omitempty" validate:"omitempty,min=1,max=255"` + KodeTarif *string `json:"kode_tarif,omitempty" validate:"omitempty,min=1,max=255"` + Uraian1 *string `json:"uraian_1,omitempty"` + Uraian2 *string `json:"uraian_2,omitempty"` + Uraian3 *string `json:"uraian_3,omitempty"` + Tarif *string `json:"tarif,omitempty" validate:"omitempty,numeric"` + Satuan *string `json:"satuan,omitempty" validate:"omitempty,min=1,max=255"` + TarifOvertime *string `json:"tarif_overtime,omitempty" validate:"omitempty,numeric"` + SatuanOvertime *string `json:"satuan_overtime,omitempty" validate:"omitempty,min=1,max=255"` + RekeningPokok *string `json:"rekening_pokok,omitempty" validate:"omitempty,min=1,max=255"` + RekeningDenda *string `json:"rekening_denda,omitempty" validate:"omitempty,min=1,max=255"` +} + +// Response struct untuk update +type RetribusiUpdateResponse struct { + Message string `json:"message"` + Data *Retribusi `json:"data"` +} + +// Response struct untuk delete +type RetribusiDeleteResponse struct { + Message string `json:"message"` + ID string `json:"id"` +} + +// Enhanced GET response dengan pagination dan aggregation +type RetribusiGetResponse struct { + Message string `json:"message"` + Data []Retribusi `json:"data"` + Meta models.MetaResponse `json:"meta"` + Summary *models.AggregateData `json:"summary,omitempty"` +} + +// Filter struct untuk query parameters +type RetribusiFilter struct { + Status *string `json:"status,omitempty" form:"status"` + Jenis *string `json:"jenis,omitempty" form:"jenis"` + Dinas *string `json:"dinas,omitempty" form:"dinas"` + KelompokObyek *string `json:"kelompok_obyek,omitempty" form:"kelompok_obyek"` + Search *string `json:"search,omitempty" form:"search"` + DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"` + DateTo *time.Time `json:"date_to,omitempty" form:"date_to"` +} diff --git a/internal/models/validation.go b/internal/models/validation.go new file mode 100644 index 0000000..1462d35 --- /dev/null +++ b/internal/models/validation.go @@ -0,0 +1,106 @@ +package models + +import ( + "regexp" + "strings" + "time" + + "github.com/go-playground/validator/v10" +) + +// CustomValidator wraps the validator +type CustomValidator struct { + Validator *validator.Validate +} + +// Validate validates struct +func (cv *CustomValidator) Validate(i interface{}) error { + return cv.Validator.Struct(i) +} + +// RegisterCustomValidations registers custom validation rules +func RegisterCustomValidations(v *validator.Validate) { + // Validate Indonesian phone number + v.RegisterValidation("indonesian_phone", validateIndonesianPhone) + + // Validate BPJS card number format + v.RegisterValidation("bpjs_card", validateBPJSCard) + + // Validate Indonesian NIK + v.RegisterValidation("indonesian_nik", validateIndonesianNIK) + + // Validate date format YYYY-MM-DD + v.RegisterValidation("date_format", validateDateFormat) + + // Validate ICD-10 code format + v.RegisterValidation("icd10", validateICD10) + + // Validate ICD-9-CM procedure code + v.RegisterValidation("icd9cm", validateICD9CM) +} + +func validateIndonesianPhone(fl validator.FieldLevel) bool { + phone := fl.Field().String() + if phone == "" { + return true // Optional field + } + + // Indonesian phone number pattern: +62, 62, 08, or 8 + pattern := `^(\+?62|0?8)[1-9][0-9]{7,11}$` + matched, _ := regexp.MatchString(pattern, phone) + return matched +} + +func validateBPJSCard(fl validator.FieldLevel) bool { + card := fl.Field().String() + if len(card) != 13 { + return false + } + + // BPJS card should be numeric + pattern := `^\d{13}$` + matched, _ := regexp.MatchString(pattern, card) + return matched +} + +func validateIndonesianNIK(fl validator.FieldLevel) bool { + nik := fl.Field().String() + if len(nik) != 16 { + return false + } + + // NIK should be numeric + pattern := `^\d{16}$` + matched, _ := regexp.MatchString(pattern, nik) + return matched +} + +func validateDateFormat(fl validator.FieldLevel) bool { + dateStr := fl.Field().String() + _, err := time.Parse("2006-01-02", dateStr) + return err == nil +} + +func validateICD10(fl validator.FieldLevel) bool { + code := fl.Field().String() + if code == "" { + return true + } + + // Basic ICD-10 pattern: Letter followed by 2 digits, optional dot and more digits + pattern := `^[A-Z]\d{2}(\.\d+)?$` + matched, _ := regexp.MatchString(pattern, strings.ToUpper(code)) + return matched +} + +func validateICD9CM(fl validator.FieldLevel) bool { + code := fl.Field().String() + if code == "" { + return true + } + + // Basic ICD-9-CM procedure pattern: 2-4 digits with optional decimal + pattern := `^\d{2,4}(\.\d+)?$` + matched, _ := regexp.MatchString(pattern, code) + return matched +} diff --git a/internal/routes/v1/routes.go b/internal/routes/v1/routes.go new file mode 100644 index 0000000..77d22c6 --- /dev/null +++ b/internal/routes/v1/routes.go @@ -0,0 +1,774 @@ +package v1 + +import ( + "api-service/internal/config" + "api-service/internal/database" + authHandlers "api-service/internal/handlers/auth" + healthcheckHandlers "api-service/internal/handlers/healthcheck" + retribusiHandlers "api-service/internal/handlers/retribusi" + "api-service/internal/handlers/websocket" + websocketHandlers "api-service/internal/handlers/websocket" + "api-service/internal/middleware" + services "api-service/internal/services/auth" + "api-service/pkg/logger" + "encoding/json" + "strconv" + "time" + + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +func RegisterRoutes(cfg *config.Config) *gin.Engine { + router := gin.New() + + // Initialize auth middleware configuration + middleware.InitializeAuth(cfg) + + // Add global middleware + router.Use(middleware.CORSConfig()) + router.Use(middleware.ErrorHandler()) + router.Use(logger.RequestLoggerMiddleware(logger.Default())) + router.Use(gin.Recovery()) + + // Initialize services with error handling + authService := services.NewAuthService(cfg) + if authService == nil { + logger.Fatal("Failed to initialize auth service") + } + + // Initialize database service + dbService := database.New(cfg) + + // Initialize WebSocket handler with enhanced features + websocketHandler := websocketHandlers.NewWebSocketHandler(cfg, dbService) + + // ============================================================================= + // HEALTH CHECK & SYSTEM ROUTES + // ============================================================================= + + healthCheckHandler := healthcheckHandlers.NewHealthCheckHandler(dbService) + sistem := router.Group("/api/sistem") + { + sistem.GET("/health", healthCheckHandler.CheckHealth) + sistem.GET("/databases", func(c *gin.Context) { + c.JSON(200, gin.H{ + "databases": dbService.ListDBs(), + "health": dbService.Health(), + "timestamp": time.Now().Unix(), + }) + }) + sistem.GET("/info", func(c *gin.Context) { + c.JSON(200, gin.H{ + "service": "API Service v1.0.0", + "websocket_active": true, + "connected_clients": websocketHandler.GetConnectedClients(), + "databases": dbService.ListDBs(), + "timestamp": time.Now().Unix(), + }) + }) + } + + // ============================================================================= + // SWAGGER DOCUMENTATION + // ============================================================================= + + router.GET("/swagger/*any", ginSwagger.WrapHandler( + swaggerFiles.Handler, + ginSwagger.DefaultModelsExpandDepth(-1), + ginSwagger.DeepLinking(true), + )) + + // ============================================================================= + // WEBSOCKET TEST CLIENT + // ============================================================================= + + // router.GET("/websocket-test", func(c *gin.Context) { + // c.Header("Content-Type", "text/html") + // c.String(http.StatusOK, getWebSocketTestHTML()) + // }) + + // ============================================================================= + // API v1 GROUP + // ============================================================================= + + v1 := router.Group("/api/v1") + + // ============================================================================= + // PUBLIC ROUTES (No Authentication Required) + // ============================================================================= + + // Authentication routes + authHandler := authHandlers.NewAuthHandler(authService) + tokenHandler := authHandlers.NewTokenHandler(authService) + + // Basic auth routes + v1.POST("/auth/login", authHandler.Login) + v1.POST("/auth/register", authHandler.Register) + v1.POST("/auth/refresh", authHandler.RefreshToken) + + // Token generation routes + v1.POST("/token/generate", tokenHandler.GenerateToken) + v1.POST("/token/generate-direct", tokenHandler.GenerateTokenDirect) + + // ============================================================================= + // WEBSOCKET ROUTES + // ============================================================================= + + // Main WebSocket endpoint with enhanced features + v1.GET("/ws", websocketHandler.HandleWebSocket) + + // WebSocket management API + wsAPI := router.Group("/api/websocket") + { + // ============================================================================= + // BASIC BROADCASTING + // ============================================================================= + + wsAPI.POST("/broadcast", func(c *gin.Context) { + var req struct { + Type string `json:"type"` + Message interface{} `json:"message"` + Database string `json:"database,omitempty"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + websocketHandler.BroadcastMessage(req.Type, req.Message) + c.JSON(200, gin.H{ + "status": "broadcast sent", + "clients_count": websocketHandler.GetConnectedClients(), + "timestamp": time.Now().Unix(), + }) + }) + + wsAPI.POST("/broadcast/room/:room", func(c *gin.Context) { + room := c.Param("room") + var req struct { + Type string `json:"type"` + Message interface{} `json:"message"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + websocketHandler.BroadcastToRoom(room, req.Type, req.Message) + c.JSON(200, gin.H{ + "status": "room broadcast sent", + "room": room, + "clients_count": websocketHandler.GetRoomClientCount(room), // Fix: gunakan GetRoomClientCount + "timestamp": time.Now().Unix(), + }) + }) + + // ============================================================================= + // ENHANCED CLIENT TARGETING + // ============================================================================= + + wsAPI.POST("/send/:clientId", func(c *gin.Context) { + clientID := c.Param("clientId") + var req struct { + Type string `json:"type"` + Message interface{} `json:"message"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + websocketHandler.SendToClient(clientID, req.Type, req.Message) + c.JSON(200, gin.H{ + "status": "message sent", + "client_id": clientID, + "timestamp": time.Now().Unix(), + }) + }) + + // Send to client by static ID + wsAPI.POST("/send/static/:staticId", func(c *gin.Context) { + staticID := c.Param("staticId") + logger.Infof("Sending message to static client: %s", staticID) + var req struct { + Type string `json:"type"` + Message interface{} `json:"message"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + success := websocketHandler.SendToClientByStaticID(staticID, req.Type, req.Message) + if success { + c.JSON(200, gin.H{ + "status": "message sent to static client", + "static_id": staticID, + "timestamp": time.Now().Unix(), + }) + } else { + c.JSON(404, gin.H{ + "error": "static client not found", + "static_id": staticID, + "timestamp": time.Now().Unix(), + }) + } + }) + + // Broadcast to all clients from specific IP + wsAPI.POST("/broadcast/ip/:ipAddress", func(c *gin.Context) { + ipAddress := c.Param("ipAddress") + var req struct { + Type string `json:"type"` + Message interface{} `json:"message"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + count := websocketHandler.BroadcastToIP(ipAddress, req.Type, req.Message) + c.JSON(200, gin.H{ + "status": "ip broadcast sent", + "ip_address": ipAddress, + "clients_count": count, + "timestamp": time.Now().Unix(), + }) + }) + + // ============================================================================= + // CLIENT INFORMATION & STATISTICS + // ============================================================================= + + wsAPI.GET("/stats", func(c *gin.Context) { + c.JSON(200, gin.H{ + "connected_clients": websocketHandler.GetConnectedClients(), + "databases": dbService.ListDBs(), + "database_health": dbService.Health(), + "timestamp": time.Now().Unix(), + }) + }) + + wsAPI.GET("/stats/detailed", func(c *gin.Context) { + stats := websocketHandler.GetDetailedStats() + c.JSON(200, gin.H{ + "stats": stats, + "timestamp": time.Now().Unix(), + }) + }) + + wsAPI.GET("/clients", func(c *gin.Context) { + clients := websocketHandler.GetAllClients() + c.JSON(200, gin.H{ + "clients": clients, + "count": len(clients), + "timestamp": time.Now().Unix(), + }) + }) + + // Fix: Perbaiki GetClientsByIP untuk menggunakan ClientInfo + wsAPI.GET("/clients/by-ip/:ipAddress", func(c *gin.Context) { + ipAddress := c.Param("ipAddress") + client := websocketHandler.GetClientsByIP(ipAddress) + if client == nil { + c.JSON(404, gin.H{ + "error": "client not found", + "ip_address": ipAddress, + "timestamp": time.Now().Unix(), + }) + return + } + + // Use ClientInfo struct instead of direct field access + clientInfo := websocketHandler.GetAllClients() + var targetClientInfo *websocket.ClientInfo + for i := range clientInfo { + if clientInfo[i].ID == ipAddress { + targetClientInfo = &clientInfo[i] + break + } + } + + if targetClientInfo == nil { + c.JSON(404, gin.H{ + "error": "ipAddress not found", + "client_id": ipAddress, + "timestamp": time.Now().Unix(), + }) + return + } + + c.JSON(200, gin.H{ + "client": map[string]interface{}{ + "id": targetClientInfo.ID, + "static_id": targetClientInfo.StaticID, + "ip_address": targetClientInfo.IPAddress, + "user_id": targetClientInfo.UserID, + "room": targetClientInfo.Room, + "connected_at": targetClientInfo.ConnectedAt.Unix(), // Fixed: use exported field + "last_ping": targetClientInfo.LastPing.Unix(), // Fixed: use exported field + }, + "timestamp": time.Now().Unix(), + }) + + }) + + // Fix: Perbaiki GetClientByID response + wsAPI.GET("/client/:clientId", func(c *gin.Context) { + clientID := c.Param("clientId") + client := websocketHandler.GetClientByID(clientID) + + if client == nil { + c.JSON(404, gin.H{ + "error": "client not found", + "client_id": clientID, + "timestamp": time.Now().Unix(), + }) + return + } + + // Use ClientInfo struct instead of direct field access + clientInfo := websocketHandler.GetAllClients() + var targetClientInfo *websocket.ClientInfo + for i := range clientInfo { + if clientInfo[i].ID == clientID { + targetClientInfo = &clientInfo[i] + break + } + } + + if targetClientInfo == nil { + c.JSON(404, gin.H{ + "error": "client not found", + "client_id": clientID, + "timestamp": time.Now().Unix(), + }) + return + } + + c.JSON(200, gin.H{ + "client": map[string]interface{}{ + "id": targetClientInfo.ID, + "static_id": targetClientInfo.StaticID, + "ip_address": targetClientInfo.IPAddress, + "user_id": targetClientInfo.UserID, + "room": targetClientInfo.Room, + "connected_at": targetClientInfo.ConnectedAt.Unix(), // Fixed: use exported field + "last_ping": targetClientInfo.LastPing.Unix(), // Fixed: use exported field + }, + "timestamp": time.Now().Unix(), + }) + }) + + // Fix: Perbaiki GetClientByStaticID response + wsAPI.GET("/client/static/:staticId", func(c *gin.Context) { + staticID := c.Param("staticId") + client := websocketHandler.GetClientByStaticID(staticID) + + if client == nil { + c.JSON(404, gin.H{ + "error": "static client not found", + "static_id": staticID, + "timestamp": time.Now().Unix(), + }) + return + } + + // Use ClientInfo struct instead of direct field access + clientInfo := websocketHandler.GetAllClients() + var targetClientInfo *websocket.ClientInfo + for i := range clientInfo { + if clientInfo[i].StaticID == staticID { + targetClientInfo = &clientInfo[i] + break + } + } + + if targetClientInfo == nil { + c.JSON(404, gin.H{ + "error": "static client not found", + "static_id": staticID, + "timestamp": time.Now().Unix(), + }) + return + } + + c.JSON(200, gin.H{ + "client": map[string]interface{}{ + "id": targetClientInfo.ID, + "static_id": targetClientInfo.StaticID, + "ip_address": targetClientInfo.IPAddress, + "user_id": targetClientInfo.UserID, + "room": targetClientInfo.Room, + "connected_at": targetClientInfo.ConnectedAt.Unix(), // Fixed: use exported field + "last_ping": targetClientInfo.LastPing.Unix(), // Fixed: use exported field + }, + "timestamp": time.Now().Unix(), + }) + }) + + // ============================================================================= + // ACTIVE CLIENTS & CLEANUP + // ============================================================================= + + // Tambahkan endpoint untuk active clients + wsAPI.GET("/clients/active", func(c *gin.Context) { + // Default: clients active dalam 5 menit terakhir + minutes := c.DefaultQuery("minutes", "5") + minutesInt, err := strconv.Atoi(minutes) + if err != nil { + minutesInt = 5 + } + + activeClients := websocketHandler.GetActiveClients(time.Duration(minutesInt) * time.Minute) + c.JSON(200, gin.H{ + "active_clients": activeClients, + "count": len(activeClients), + "threshold_minutes": minutesInt, + "timestamp": time.Now().Unix(), + }) + }) + + // Tambahkan endpoint untuk cleanup inactive clients + wsAPI.POST("/cleanup/inactive", func(c *gin.Context) { + var req struct { + InactiveMinutes int `json:"inactive_minutes"` + } + if err := c.ShouldBindJSON(&req); err != nil { + req.InactiveMinutes = 30 // Default 30 minutes + } + + if req.InactiveMinutes <= 0 { + req.InactiveMinutes = 30 + } + + cleanedCount := websocketHandler.CleanupInactiveClients(time.Duration(req.InactiveMinutes) * time.Minute) + c.JSON(200, gin.H{ + "status": "cleanup completed", + "cleaned_clients": cleanedCount, + "inactive_minutes": req.InactiveMinutes, + "timestamp": time.Now().Unix(), + }) + }) + + // ============================================================================= + // DATABASE NOTIFICATIONS + // ============================================================================= + + wsAPI.POST("/notify/:database/:channel", func(c *gin.Context) { + database := c.Param("database") + channel := c.Param("channel") + + var req struct { + Payload interface{} `json:"payload"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + payloadJSON, _ := json.Marshal(req.Payload) + err := dbService.NotifyChange(database, channel, string(payloadJSON)) + if err != nil { + c.JSON(500, gin.H{ + "error": err.Error(), + "database": database, + "channel": channel, + "timestamp": time.Now().Unix(), + }) + return + } + + c.JSON(200, gin.H{ + "status": "notification sent", + "database": database, + "channel": channel, + "timestamp": time.Now().Unix(), + }) + }) + + // Test database notification + wsAPI.POST("/test-notification", func(c *gin.Context) { + var req struct { + Database string `json:"database"` + Channel string `json:"channel"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + // Default values + if req.Database == "" { + req.Database = "default" + } + if req.Channel == "" { + req.Channel = "system_changes" + } + if req.Message == "" { + req.Message = "Test notification from API" + } + + payload := map[string]interface{}{ + "operation": "API_TEST", + "table": "manual_test", + "data": map[string]interface{}{ + "message": req.Message, + "test_data": req.Data, + "timestamp": time.Now().Unix(), + }, + } + + payloadJSON, _ := json.Marshal(payload) + err := dbService.NotifyChange(req.Database, req.Channel, string(payloadJSON)) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + + c.JSON(200, gin.H{ + "status": "test notification sent", + "database": req.Database, + "channel": req.Channel, + "payload": payload, + "timestamp": time.Now().Unix(), + }) + }) + + // ============================================================================= + // ROOM MANAGEMENT + // ============================================================================= + + wsAPI.GET("/rooms", func(c *gin.Context) { + rooms := websocketHandler.GetAllRooms() + c.JSON(200, gin.H{ + "rooms": rooms, + "count": len(rooms), + "timestamp": time.Now().Unix(), + }) + }) + + wsAPI.GET("/room/:room/clients", func(c *gin.Context) { + room := c.Param("room") + clientCount := websocketHandler.GetRoomClientCount(room) + + // Get detailed room info + allRooms := websocketHandler.GetAllRooms() + roomClients := allRooms[room] + + c.JSON(200, gin.H{ + "room": room, + "client_count": clientCount, + "clients": roomClients, + "timestamp": time.Now().Unix(), + }) + }) + + // ============================================================================= + // MONITORING & DEBUGGING + // ============================================================================= + + wsAPI.GET("/monitor", func(c *gin.Context) { + monitor := websocketHandler.GetMonitoringData() + c.JSON(200, monitor) + }) + + wsAPI.POST("/ping-client/:clientId", func(c *gin.Context) { + clientID := c.Param("clientId") + websocketHandler.SendToClient(clientID, "server_ping", map[string]interface{}{ + "message": "Ping from server", + "timestamp": time.Now().Unix(), + }) + c.JSON(200, gin.H{ + "status": "ping sent", + "client_id": clientID, + "timestamp": time.Now().Unix(), + }) + }) + + // Disconnect specific client + wsAPI.POST("/disconnect/:clientId", func(c *gin.Context) { + clientID := c.Param("clientId") + success := websocketHandler.DisconnectClient(clientID) + if success { + c.JSON(200, gin.H{ + "status": "client disconnected", + "client_id": clientID, + "timestamp": time.Now().Unix(), + }) + } else { + c.JSON(404, gin.H{ + "error": "client not found", + "client_id": clientID, + "timestamp": time.Now().Unix(), + }) + } + }) + + // ============================================================================= + // BULK OPERATIONS + // ============================================================================= + + // Broadcast to multiple clients + wsAPI.POST("/broadcast/bulk", func(c *gin.Context) { + var req struct { + ClientIDs []string `json:"client_ids"` + Type string `json:"type"` + Message interface{} `json:"message"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + successCount := 0 + for _, clientID := range req.ClientIDs { + websocketHandler.SendToClient(clientID, req.Type, req.Message) + successCount++ + } + + c.JSON(200, gin.H{ + "status": "bulk broadcast sent", + "total_clients": len(req.ClientIDs), + "success_count": successCount, + "timestamp": time.Now().Unix(), + }) + }) + + // Disconnect multiple clients + wsAPI.POST("/disconnect/bulk", func(c *gin.Context) { + var req struct { + ClientIDs []string `json:"client_ids"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + successCount := 0 + for _, clientID := range req.ClientIDs { + if websocketHandler.DisconnectClient(clientID) { + successCount++ + } + } + + c.JSON(200, gin.H{ + "status": "bulk disconnect completed", + "total_clients": len(req.ClientIDs), + "success_count": successCount, + "timestamp": time.Now().Unix(), + }) + }) + } + + // ============================================================================= + // PUBLISHED ROUTES + // ============================================================================= + + // Retribusi endpoints with WebSocket notifications + retribusiHandler := retribusiHandlers.NewRetribusiHandler() + retribusiGroup := v1.Group("/retribusi") + { + retribusiGroup.GET("", retribusiHandler.GetRetribusi) + retribusiGroup.GET("/dynamic", retribusiHandler.GetRetribusiDynamic) + retribusiGroup.GET("/search", retribusiHandler.SearchRetribusiAdvanced) + retribusiGroup.GET("/id/:id", retribusiHandler.GetRetribusiByID) + + // POST/PUT/DELETE with automatic WebSocket notifications + retribusiGroup.POST("", func(c *gin.Context) { + retribusiHandler.CreateRetribusi(c) + + // Trigger WebSocket notification after successful creation + if c.Writer.Status() == 200 || c.Writer.Status() == 201 { + websocketHandler.BroadcastMessage("retribusi_created", map[string]interface{}{ + "message": "New retribusi record created", + "timestamp": time.Now().Unix(), + }) + } + }) + + retribusiGroup.PUT("/id/:id", func(c *gin.Context) { + id := c.Param("id") + retribusiHandler.UpdateRetribusi(c) + + // Trigger WebSocket notification after successful update + if c.Writer.Status() == 200 { + websocketHandler.BroadcastMessage("retribusi_updated", map[string]interface{}{ + "message": "Retribusi record updated", + "id": id, + "timestamp": time.Now().Unix(), + }) + } + }) + + retribusiGroup.DELETE("/id/:id", func(c *gin.Context) { + id := c.Param("id") + retribusiHandler.DeleteRetribusi(c) + + // Trigger WebSocket notification after successful deletion + if c.Writer.Status() == 200 { + websocketHandler.BroadcastMessage("retribusi_deleted", map[string]interface{}{ + "message": "Retribusi record deleted", + "id": id, + "timestamp": time.Now().Unix(), + }) + } + }) + } + + // ============================================================================= + // PROTECTED ROUTES (Authentication Required) + // ============================================================================= + + protected := v1.Group("/") + protected.Use(middleware.ConfigurableAuthMiddleware(cfg)) + + // Protected WebSocket management (optional) + protectedWS := protected.Group("/ws-admin") + { + protectedWS.GET("/stats", func(c *gin.Context) { + detailedStats := websocketHandler.GetDetailedStats() + c.JSON(200, gin.H{ + "admin_stats": detailedStats, + "timestamp": time.Now().Unix(), + }) + }) + + protectedWS.POST("/force-disconnect/:clientId", func(c *gin.Context) { + clientID := c.Param("clientId") + success := websocketHandler.DisconnectClient(clientID) + c.JSON(200, gin.H{ + "status": "force disconnect attempted", + "client_id": clientID, + "success": success, + "timestamp": time.Now().Unix(), + }) + }) + + protectedWS.POST("/cleanup/force", func(c *gin.Context) { + var req struct { + InactiveMinutes int `json:"inactive_minutes"` + Force bool `json:"force"` + } + if err := c.ShouldBindJSON(&req); err != nil { + req.InactiveMinutes = 10 + req.Force = false + } + + cleanedCount := websocketHandler.CleanupInactiveClients(time.Duration(req.InactiveMinutes) * time.Minute) + c.JSON(200, gin.H{ + "status": "admin cleanup completed", + "cleaned_clients": cleanedCount, + "inactive_minutes": req.InactiveMinutes, + "force": req.Force, + "timestamp": time.Now().Unix(), + }) + }) + } + + return router +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..98ef90c --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,53 @@ +package server + +import ( + "fmt" + "net/http" + "os" + "strconv" + "time" + + _ "github.com/joho/godotenv/autoload" + + "api-service/internal/config" + "api-service/internal/database" + v1 "api-service/internal/routes/v1" +) + +var dbService database.Service // Global variable to hold the database service instance + +type Server struct { + port int + db database.Service +} + +func NewServer() *http.Server { + // Load configuration + cfg := config.LoadConfig() + cfg.Validate() + + port, _ := strconv.Atoi(os.Getenv("PORT")) + if port == 0 { + port = cfg.Server.Port + } + + if dbService == nil { // Check if the database service is already initialized + dbService = database.New(cfg) // Initialize only once + } + + NewServer := &Server{ + port: port, + db: dbService, // Use the global database service instance + } + + // Declare Server config + server := &http.Server{ + Addr: fmt.Sprintf(":%d", NewServer.port), + Handler: v1.RegisterRoutes(cfg), + IdleTimeout: time.Minute, + ReadTimeout: 10 * time.Second, + WriteTimeout: 30 * time.Second, + } + + return server +} diff --git a/internal/services/auth/auth.go b/internal/services/auth/auth.go new file mode 100644 index 0000000..d76aadb --- /dev/null +++ b/internal/services/auth/auth.go @@ -0,0 +1,169 @@ +package services + +import ( + "api-service/internal/config" + models "api-service/internal/models/auth" + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" +) + +// AuthService handles authentication logic +type AuthService struct { + config *config.Config + users map[string]*models.User // In-memory user store for demo +} + +// NewAuthService creates a new authentication service +func NewAuthService(cfg *config.Config) *AuthService { + // Initialize with demo users + users := make(map[string]*models.User) + + // Add demo users + users["admin"] = &models.User{ + ID: "1", + Username: "admin", + Email: "admin@example.com", + Password: "$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi", // password + Role: "admin", + } + + users["user"] = &models.User{ + ID: "2", + Username: "user", + Email: "user@example.com", + Password: "$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi", // password + Role: "user", + } + + return &AuthService{ + config: cfg, + users: users, + } +} + +// Login authenticates user and generates JWT token +func (s *AuthService) Login(username, password string) (*models.TokenResponse, error) { + user, exists := s.users[username] + if !exists { + return nil, errors.New("invalid credentials") + } + + // Verify password + err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) + if err != nil { + return nil, errors.New("invalid credentials") + } + + // Generate JWT token + token, err := s.generateToken(user) + if err != nil { + return nil, err + } + + return &models.TokenResponse{ + AccessToken: token, + TokenType: "Bearer", + ExpiresIn: 3600, // 1 hour + }, nil +} + +// generateToken creates a new JWT token for the user +func (s *AuthService) generateToken(user *models.User) (string, error) { + // Create claims + claims := jwt.MapClaims{ + "user_id": user.ID, + "username": user.Username, + "email": user.Email, + "role": user.Role, + "exp": time.Now().Add(time.Hour * 1).Unix(), + "iat": time.Now().Unix(), + } + + // Create token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Sign token with secret key + secretKey := []byte(s.getJWTSecret()) + return token.SignedString(secretKey) +} + +// GenerateTokenForUser generates a JWT token for a specific user +func (s *AuthService) GenerateTokenForUser(user *models.User) (string, error) { + // Create claims + claims := jwt.MapClaims{ + "user_id": user.ID, + "username": user.Username, + "email": user.Email, + "role": user.Role, + "exp": time.Now().Add(time.Hour * 1).Unix(), + "iat": time.Now().Unix(), + } + + // Create token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Sign token with secret key + secretKey := []byte(s.getJWTSecret()) + return token.SignedString(secretKey) +} + +// ValidateToken validates the JWT token +func (s *AuthService) ValidateToken(tokenString string) (*models.JWTClaims, error) { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("unexpected signing method") + } + return []byte(s.getJWTSecret()), nil + }) + + if err != nil { + return nil, err + } + + if !token.Valid { + return nil, errors.New("invalid token") + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return nil, errors.New("invalid claims") + } + + return &models.JWTClaims{ + UserID: claims["user_id"].(string), + Username: claims["username"].(string), + Email: claims["email"].(string), + Role: claims["role"].(string), + }, nil +} + +// getJWTSecret returns the JWT secret key +func (s *AuthService) getJWTSecret() string { + // In production, this should come from environment variables + return "your-secret-key-change-this-in-production" +} + +// RegisterUser registers a new user (for demo purposes) +func (s *AuthService) RegisterUser(username, email, password, role string) error { + if _, exists := s.users[username]; exists { + return errors.New("username already exists") + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return err + } + + s.users[username] = &models.User{ + ID: string(rune(len(s.users) + 1)), + Username: username, + Email: email, + Password: string(hashedPassword), + Role: role, + } + + return nil +} diff --git a/internal/utils/filters/dynamic_filter.go b/internal/utils/filters/dynamic_filter.go new file mode 100644 index 0000000..d735ce2 --- /dev/null +++ b/internal/utils/filters/dynamic_filter.go @@ -0,0 +1,593 @@ +package utils + +import ( + "fmt" + "reflect" + "strings" + "sync" +) + +// FilterOperator represents supported filter operators +type FilterOperator string + +const ( + OpEqual FilterOperator = "_eq" + OpNotEqual FilterOperator = "_neq" + OpLike FilterOperator = "_like" + OpILike FilterOperator = "_ilike" + OpIn FilterOperator = "_in" + OpNotIn FilterOperator = "_nin" + OpGreaterThan FilterOperator = "_gt" + OpGreaterThanEqual FilterOperator = "_gte" + OpLessThan FilterOperator = "_lt" + OpLessThanEqual FilterOperator = "_lte" + OpBetween FilterOperator = "_between" + OpNotBetween FilterOperator = "_nbetween" + OpNull FilterOperator = "_null" + OpNotNull FilterOperator = "_nnull" + OpContains FilterOperator = "_contains" + OpNotContains FilterOperator = "_ncontains" + OpStartsWith FilterOperator = "_starts_with" + OpEndsWith FilterOperator = "_ends_with" +) + +// DynamicFilter represents a single filter condition +type DynamicFilter struct { + Column string `json:"column"` + Operator FilterOperator `json:"operator"` + Value interface{} `json:"value"` + LogicOp string `json:"logic_op,omitempty"` // AND, OR +} + +// FilterGroup represents a group of filters +type FilterGroup struct { + Filters []DynamicFilter `json:"filters"` + LogicOp string `json:"logic_op"` // AND, OR +} + +// DynamicQuery represents the complete query structure +type DynamicQuery struct { + Fields []string `json:"fields,omitempty"` + Filters []FilterGroup `json:"filters,omitempty"` + Sort []SortField `json:"sort,omitempty"` + Limit int `json:"limit"` + Offset int `json:"offset"` + GroupBy []string `json:"group_by,omitempty"` + Having []FilterGroup `json:"having,omitempty"` +} + +// SortField represents sorting configuration +type SortField struct { + Column string `json:"column"` + Order string `json:"order"` // ASC, DESC +} + +// QueryBuilder builds SQL queries from dynamic filters +type QueryBuilder struct { + tableName string + columnMapping map[string]string // Maps API field names to DB column names + allowedColumns map[string]bool // Security: only allow specified columns + paramCounter int + mu *sync.RWMutex +} + +// NewQueryBuilder creates a new query builder instance +func NewQueryBuilder(tableName string) *QueryBuilder { + return &QueryBuilder{ + tableName: tableName, + columnMapping: make(map[string]string), + allowedColumns: make(map[string]bool), + paramCounter: 0, + } +} + +// SetColumnMapping sets the mapping between API field names and database column names +func (qb *QueryBuilder) SetColumnMapping(mapping map[string]string) *QueryBuilder { + qb.columnMapping = mapping + return qb +} + +// SetAllowedColumns sets the list of allowed columns for security +func (qb *QueryBuilder) SetAllowedColumns(columns []string) *QueryBuilder { + qb.allowedColumns = make(map[string]bool) + for _, col := range columns { + qb.allowedColumns[col] = true + } + return qb +} + +// BuildQuery builds the complete SQL query +func (qb *QueryBuilder) BuildQuery(query DynamicQuery) (string, []interface{}, error) { + qb.paramCounter = 0 + + // Build SELECT clause + selectClause := qb.buildSelectClause(query.Fields) + + // Build FROM clause + fromClause := fmt.Sprintf("FROM %s", qb.tableName) + + // Build WHERE clause + whereClause, whereArgs, err := qb.buildWhereClause(query.Filters) + if err != nil { + return "", nil, err + } + + // Build ORDER BY clause + orderClause := qb.buildOrderClause(query.Sort) + + // Build GROUP BY clause + groupClause := qb.buildGroupByClause(query.GroupBy) + + // Build HAVING clause + havingClause, havingArgs, err := qb.buildHavingClause(query.Having) + if err != nil { + return "", nil, err + } + + // Combine all parts + sqlParts := []string{selectClause, fromClause} + args := []interface{}{} + + if whereClause != "" { + sqlParts = append(sqlParts, "WHERE "+whereClause) + args = append(args, whereArgs...) + } + + if groupClause != "" { + sqlParts = append(sqlParts, groupClause) + } + + if havingClause != "" { + sqlParts = append(sqlParts, "HAVING "+havingClause) + args = append(args, havingArgs...) + } + + if orderClause != "" { + sqlParts = append(sqlParts, orderClause) + } + + // Add pagination + if query.Limit > 0 { + qb.paramCounter++ + sqlParts = append(sqlParts, fmt.Sprintf("LIMIT $%d", qb.paramCounter)) + args = append(args, query.Limit) + } + + if query.Offset > 0 { + qb.paramCounter++ + sqlParts = append(sqlParts, fmt.Sprintf("OFFSET $%d", qb.paramCounter)) + args = append(args, query.Offset) + } + + sql := strings.Join(sqlParts, " ") + return sql, args, nil +} + +// buildSelectClause builds the SELECT part of the query +func (qb *QueryBuilder) buildSelectClause(fields []string) string { + if len(fields) == 0 || (len(fields) == 1 && fields[0] == "*") { + return "SELECT *" + } + + var selectedFields []string + for _, field := range fields { + if field == "*.*" || field == "*" { + selectedFields = append(selectedFields, "*") + continue + } + + // Check if it's an expression (contains spaces, parentheses, etc.) + if strings.Contains(field, " ") || strings.Contains(field, "(") || strings.Contains(field, ")") { + // Expression, add as is + selectedFields = append(selectedFields, field) + continue + } + + // Security check: only allow specified columns (check original field name) + if len(qb.allowedColumns) > 0 && !qb.allowedColumns[field] { + continue + } + + // Map field name if mapping exists + if mappedCol, exists := qb.columnMapping[field]; exists { + field = mappedCol + } + + selectedFields = append(selectedFields, fmt.Sprintf(`"%s"`, field)) + } + + if len(selectedFields) == 0 { + return "SELECT *" + } + + return "SELECT " + strings.Join(selectedFields, ", ") +} + +// buildWhereClause builds the WHERE part of the query +func (qb *QueryBuilder) buildWhereClause(filterGroups []FilterGroup) (string, []interface{}, error) { + if len(filterGroups) == 0 { + return "", nil, nil + } + + var conditions []string + var args []interface{} + + for i, group := range filterGroups { + groupCondition, groupArgs, err := qb.buildFilterGroup(group) + if err != nil { + return "", nil, err + } + + if groupCondition != "" { + if i > 0 { + logicOp := "AND" + if group.LogicOp != "" { + logicOp = strings.ToUpper(group.LogicOp) + } + conditions = append(conditions, logicOp) + } + + conditions = append(conditions, groupCondition) + args = append(args, groupArgs...) + } + } + + return strings.Join(conditions, " "), args, nil +} + +// buildFilterGroup builds conditions for a filter group +func (qb *QueryBuilder) buildFilterGroup(group FilterGroup) (string, []interface{}, error) { + if len(group.Filters) == 0 { + return "", nil, nil + } + + var conditions []string + var args []interface{} + + for i, filter := range group.Filters { + condition, filterArgs, err := qb.buildFilterCondition(filter) + if err != nil { + return "", nil, err + } + + if condition != "" { + if i > 0 { + logicOp := "AND" + if filter.LogicOp != "" { + logicOp = strings.ToUpper(filter.LogicOp) + } else if group.LogicOp != "" { + logicOp = strings.ToUpper(group.LogicOp) + } + conditions = append(conditions, logicOp) + } + + conditions = append(conditions, condition) + args = append(args, filterArgs...) + } + } + + return strings.Join(conditions, " "), args, nil +} + +// buildFilterCondition builds a single filter condition +func (qb *QueryBuilder) buildFilterCondition(filter DynamicFilter) (string, []interface{}, error) { + // Security check (check original field name) + if len(qb.allowedColumns) > 0 && !qb.allowedColumns[filter.Column] { + return "", nil, nil + } + + // Map column name if mapping exists + column := filter.Column + if mappedCol, exists := qb.columnMapping[column]; exists { + column = mappedCol + } + + // Wrap column name in quotes for PostgreSQL + column = fmt.Sprintf(`"%s"`, column) + + switch filter.Operator { + case OpEqual: + if filter.Value == nil { + return "", nil, nil + } + qb.paramCounter++ + return fmt.Sprintf("%s = $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil + + case OpNotEqual: + if filter.Value == nil { + return "", nil, nil + } + qb.paramCounter++ + return fmt.Sprintf("%s != $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil + + case OpLike: + if filter.Value == nil { + return "", nil, nil + } + qb.paramCounter++ + return fmt.Sprintf("%s LIKE $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil + + case OpILike: + if filter.Value == nil { + return "", nil, nil + } + qb.paramCounter++ + return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil + + case OpIn: + values := qb.parseArrayValue(filter.Value) + if len(values) == 0 { + return "", nil, nil + } + + var placeholders []string + var args []interface{} + for _, val := range values { + qb.paramCounter++ + placeholders = append(placeholders, fmt.Sprintf("$%d", qb.paramCounter)) + args = append(args, val) + } + + return fmt.Sprintf("%s IN (%s)", column, strings.Join(placeholders, ", ")), args, nil + + case OpNotIn: + values := qb.parseArrayValue(filter.Value) + if len(values) == 0 { + return "", nil, nil + } + + var placeholders []string + var args []interface{} + for _, val := range values { + qb.paramCounter++ + placeholders = append(placeholders, fmt.Sprintf("$%d", qb.paramCounter)) + args = append(args, val) + } + + return fmt.Sprintf("%s NOT IN (%s)", column, strings.Join(placeholders, ", ")), args, nil + + case OpGreaterThan: + if filter.Value == nil { + return "", nil, nil + } + qb.paramCounter++ + return fmt.Sprintf("%s > $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil + + case OpGreaterThanEqual: + if filter.Value == nil { + return "", nil, nil + } + qb.paramCounter++ + return fmt.Sprintf("%s >= $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil + + case OpLessThan: + if filter.Value == nil { + return "", nil, nil + } + qb.paramCounter++ + return fmt.Sprintf("%s < $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil + + case OpLessThanEqual: + if filter.Value == nil { + return "", nil, nil + } + qb.paramCounter++ + return fmt.Sprintf("%s <= $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil + + case OpBetween: + if filter.Value == nil { + return "", nil, nil + } + values := qb.parseArrayValue(filter.Value) + if len(values) != 2 { + return "", nil, fmt.Errorf("between operator requires exactly 2 values") + } + qb.paramCounter++ + param1 := qb.paramCounter + qb.paramCounter++ + param2 := qb.paramCounter + return fmt.Sprintf("%s BETWEEN $%d AND $%d", column, param1, param2), []interface{}{values[0], values[1]}, nil + + case OpNotBetween: + if filter.Value == nil { + return "", nil, nil + } + values := qb.parseArrayValue(filter.Value) + if len(values) != 2 { + return "", nil, fmt.Errorf("not between operator requires exactly 2 values") + } + qb.paramCounter++ + param1 := qb.paramCounter + qb.paramCounter++ + param2 := qb.paramCounter + return fmt.Sprintf("%s NOT BETWEEN $%d AND $%d", column, param1, param2), []interface{}{values[0], values[1]}, nil + + case OpNull: + return fmt.Sprintf("%s IS NULL", column), nil, nil + + case OpNotNull: + return fmt.Sprintf("%s IS NOT NULL", column), nil, nil + + case OpContains: + if filter.Value == nil { + return "", nil, nil + } + qb.paramCounter++ + value := fmt.Sprintf("%%%v%%", filter.Value) + return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil + + case OpNotContains: + if filter.Value == nil { + return "", nil, nil + } + qb.paramCounter++ + value := fmt.Sprintf("%%%v%%", filter.Value) + return fmt.Sprintf("%s NOT ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil + + case OpStartsWith: + if filter.Value == nil { + return "", nil, nil + } + qb.paramCounter++ + value := fmt.Sprintf("%v%%", filter.Value) + return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil + + case OpEndsWith: + if filter.Value == nil { + return "", nil, nil + } + qb.paramCounter++ + value := fmt.Sprintf("%%%v", filter.Value) + return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil + + default: + return "", nil, fmt.Errorf("unsupported operator: %s", filter.Operator) + } +} + +// parseArrayValue parses array values from various formats +func (qb *QueryBuilder) parseArrayValue(value interface{}) []interface{} { + if value == nil { + return nil + } + + // If it's already a slice + if reflect.TypeOf(value).Kind() == reflect.Slice { + v := reflect.ValueOf(value) + result := make([]interface{}, v.Len()) + for i := 0; i < v.Len(); i++ { + result[i] = v.Index(i).Interface() + } + return result + } + + // If it's a string, try to split by comma + if str, ok := value.(string); ok { + if strings.Contains(str, ",") { + parts := strings.Split(str, ",") + result := make([]interface{}, len(parts)) + for i, part := range parts { + result[i] = strings.TrimSpace(part) + } + return result + } + return []interface{}{str} + } + + return []interface{}{value} +} + +// buildOrderClause builds the ORDER BY clause +func (qb *QueryBuilder) buildOrderClause(sortFields []SortField) string { + if len(sortFields) == 0 { + return "" + } + + var orderParts []string + for _, sort := range sortFields { + column := sort.Column + + // Security check (check original field name) + if len(qb.allowedColumns) > 0 && !qb.allowedColumns[column] { + continue + } + + if mappedCol, exists := qb.columnMapping[column]; exists { + column = mappedCol + } + + order := "ASC" + if sort.Order != "" { + order = strings.ToUpper(sort.Order) + } + + orderParts = append(orderParts, fmt.Sprintf(`"%s" %s`, column, order)) + } + + if len(orderParts) == 0 { + return "" + } + + return "ORDER BY " + strings.Join(orderParts, ", ") +} + +// buildGroupByClause builds the GROUP BY clause +func (qb *QueryBuilder) buildGroupByClause(groupFields []string) string { + if len(groupFields) == 0 { + return "" + } + + var groupParts []string + for _, field := range groupFields { + column := field + if mappedCol, exists := qb.columnMapping[column]; exists { + column = mappedCol + } + + // Security check + if len(qb.allowedColumns) > 0 && !qb.allowedColumns[column] { + continue + } + + groupParts = append(groupParts, fmt.Sprintf(`"%s"`, column)) + } + + if len(groupParts) == 0 { + return "" + } + + return "GROUP BY " + strings.Join(groupParts, ", ") +} + +// buildHavingClause builds the HAVING clause +func (qb *QueryBuilder) buildHavingClause(havingGroups []FilterGroup) (string, []interface{}, error) { + if len(havingGroups) == 0 { + return "", nil, nil + } + + return qb.buildWhereClause(havingGroups) +} + +// BuildCountQuery builds a count query +func (qb *QueryBuilder) BuildCountQuery(query DynamicQuery) (string, []interface{}, error) { + qb.paramCounter = 0 + + // Build FROM clause + fromClause := fmt.Sprintf("FROM %s", qb.tableName) + + // Build WHERE clause + whereClause, whereArgs, err := qb.buildWhereClause(query.Filters) + if err != nil { + return "", nil, err + } + + // Build GROUP BY clause + groupClause := qb.buildGroupByClause(query.GroupBy) + + // Build HAVING clause + havingClause, havingArgs, err := qb.buildHavingClause(query.Having) + if err != nil { + return "", nil, err + } + + // Combine parts + sqlParts := []string{"SELECT COUNT(*)", fromClause} + args := []interface{}{} + + if whereClause != "" { + sqlParts = append(sqlParts, "WHERE "+whereClause) + args = append(args, whereArgs...) + } + + if groupClause != "" { + sqlParts = append(sqlParts, groupClause) + } + + if havingClause != "" { + sqlParts = append(sqlParts, "HAVING "+havingClause) + args = append(args, havingArgs...) + } + + sql := strings.Join(sqlParts, " ") + return sql, args, nil +} diff --git a/internal/utils/filters/query_parser.go b/internal/utils/filters/query_parser.go new file mode 100644 index 0000000..6b6f07e --- /dev/null +++ b/internal/utils/filters/query_parser.go @@ -0,0 +1,241 @@ +package utils + +import ( + "net/url" + "strconv" + "strings" + "time" +) + +// QueryParser parses HTTP query parameters into DynamicQuery +type QueryParser struct { + defaultLimit int + maxLimit int +} + +// NewQueryParser creates a new query parser +func NewQueryParser() *QueryParser { + return &QueryParser{ + defaultLimit: 10, + maxLimit: 100, + } +} + +// SetLimits sets default and maximum limits +func (qp *QueryParser) SetLimits(defaultLimit, maxLimit int) *QueryParser { + qp.defaultLimit = defaultLimit + qp.maxLimit = maxLimit + return qp +} + +// ParseQuery parses URL query parameters into DynamicQuery +func (qp *QueryParser) ParseQuery(values url.Values) (DynamicQuery, error) { + query := DynamicQuery{ + Limit: qp.defaultLimit, + Offset: 0, + } + + // Parse fields + if fields := values.Get("fields"); fields != "" { + if fields == "*.*" || fields == "*" { + query.Fields = []string{"*"} + } else { + query.Fields = strings.Split(fields, ",") + for i, field := range query.Fields { + query.Fields[i] = strings.TrimSpace(field) + } + } + } + + // Parse pagination + if limit := values.Get("limit"); limit != "" { + if l, err := strconv.Atoi(limit); err == nil { + if l > 0 && l <= qp.maxLimit { + query.Limit = l + } + } + } + + if offset := values.Get("offset"); offset != "" { + if o, err := strconv.Atoi(offset); err == nil && o >= 0 { + query.Offset = o + } + } + + // Parse filters + filters, err := qp.parseFilters(values) + if err != nil { + return query, err + } + query.Filters = filters + + // Parse sorting + sorts, err := qp.parseSorting(values) + if err != nil { + return query, err + } + query.Sort = sorts + + // Parse group by + if groupBy := values.Get("group"); groupBy != "" { + query.GroupBy = strings.Split(groupBy, ",") + for i, field := range query.GroupBy { + query.GroupBy[i] = strings.TrimSpace(field) + } + } + + return query, nil +} + +// parseFilters parses filter parameters +// Supports format: filter[column][operator]=value +func (qp *QueryParser) parseFilters(values url.Values) ([]FilterGroup, error) { + filterMap := make(map[string]map[string]string) + + // Group filters by column + for key, vals := range values { + if strings.HasPrefix(key, "filter[") && strings.HasSuffix(key, "]") { + // Parse filter[column][operator] format + parts := strings.Split(key[7:len(key)-1], "][") + if len(parts) == 2 { + column := parts[0] + operator := parts[1] + + if filterMap[column] == nil { + filterMap[column] = make(map[string]string) + } + + if len(vals) > 0 { + filterMap[column][operator] = vals[0] + } + } + } + } + + if len(filterMap) == 0 { + return nil, nil + } + + // Convert to FilterGroup + var filters []DynamicFilter + + for column, operators := range filterMap { + for opStr, value := range operators { + operator := FilterOperator(opStr) + + // Parse value based on operator + var parsedValue interface{} + switch operator { + case OpIn, OpNotIn: + if value != "" { + parsedValue = strings.Split(value, ",") + } + case OpBetween, OpNotBetween: + if value != "" { + parts := strings.Split(value, ",") + if len(parts) == 2 { + parsedValue = []interface{}{strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])} + } + } + case OpNull, OpNotNull: + parsedValue = nil + default: + parsedValue = value + } + + filters = append(filters, DynamicFilter{ + Column: column, + Operator: operator, + Value: parsedValue, + }) + } + } + + if len(filters) == 0 { + return nil, nil + } + + return []FilterGroup{{ + Filters: filters, + LogicOp: "AND", + }}, nil +} + +// parseSorting parses sort parameters +// Supports format: sort=column1,-column2 (- for DESC) +func (qp *QueryParser) parseSorting(values url.Values) ([]SortField, error) { + sortParam := values.Get("sort") + if sortParam == "" { + return nil, nil + } + + var sorts []SortField + fields := strings.Split(sortParam, ",") + + for _, field := range fields { + field = strings.TrimSpace(field) + if field == "" { + continue + } + + order := "ASC" + column := field + + if strings.HasPrefix(field, "-") { + order = "DESC" + column = field[1:] + } else if strings.HasPrefix(field, "+") { + column = field[1:] + } + + sorts = append(sorts, SortField{ + Column: column, + Order: order, + }) + } + + return sorts, nil +} + +// ParseAdvancedFilters parses complex filter structures +// Supports nested filters and logic operators +func (qp *QueryParser) ParseAdvancedFilters(filterParam string) ([]FilterGroup, error) { + // This would be for more complex JSON-based filters + // Implementation depends on your specific needs + return nil, nil +} + +// Helper function to parse date values +func parseDate(value string) (interface{}, error) { + // Try different date formats + formats := []string{ + "2006-01-02", + "2006-01-02T15:04:05Z", + "2006-01-02T15:04:05.000Z", + "2006-01-02 15:04:05", + } + + for _, format := range formats { + if t, err := time.Parse(format, value); err == nil { + return t, nil + } + } + + return value, nil +} + +// Helper function to parse numeric values +func parseNumeric(value string) interface{} { + // Try integer first + if i, err := strconv.Atoi(value); err == nil { + return i + } + + // Try float + if f, err := strconv.ParseFloat(value, 64); err == nil { + return f + } + + // Return as string + return value +} diff --git a/internal/utils/validation/duplicate_validator.go b/internal/utils/validation/duplicate_validator.go new file mode 100644 index 0000000..863c058 --- /dev/null +++ b/internal/utils/validation/duplicate_validator.go @@ -0,0 +1,141 @@ +package validation + +import ( + "context" + "database/sql" + "fmt" + "time" +) + +// ValidationConfig holds configuration for duplicate validation +type ValidationConfig struct { + TableName string + IDColumn string + StatusColumn string + DateColumn string + ActiveStatuses []string + AdditionalFields map[string]interface{} +} + +// DuplicateValidator provides methods for validating duplicate entries +type DuplicateValidator struct { + db *sql.DB +} + +// NewDuplicateValidator creates a new instance of DuplicateValidator +func NewDuplicateValidator(db *sql.DB) *DuplicateValidator { + return &DuplicateValidator{db: db} +} + +// ValidateDuplicate checks for duplicate entries based on the provided configuration +func (dv *DuplicateValidator) ValidateDuplicate(ctx context.Context, config ValidationConfig, identifier interface{}) error { + query := fmt.Sprintf(` + SELECT COUNT(*) + FROM %s + WHERE %s = $1 + AND %s = ANY($2) + AND DATE(%s) = CURRENT_DATE + `, config.TableName, config.IDColumn, config.StatusColumn, config.DateColumn) + + var count int + err := dv.db.QueryRowContext(ctx, query, identifier, config.ActiveStatuses).Scan(&count) + if err != nil { + return fmt.Errorf("failed to check duplicate: %w", err) + } + + if count > 0 { + return fmt.Errorf("data with ID %v already exists with active status today", identifier) + } + + return nil +} + +// ValidateDuplicateWithCustomFields checks for duplicates with additional custom fields +func (dv *DuplicateValidator) ValidateDuplicateWithCustomFields(ctx context.Context, config ValidationConfig, fields map[string]interface{}) error { + whereClause := fmt.Sprintf("%s = ANY($1) AND DATE(%s) = CURRENT_DATE", config.StatusColumn, config.DateColumn) + args := []interface{}{config.ActiveStatuses} + argIndex := 2 + + // Add additional field conditions + for fieldName, fieldValue := range config.AdditionalFields { + whereClause += fmt.Sprintf(" AND %s = $%d", fieldName, argIndex) + args = append(args, fieldValue) + argIndex++ + } + + // Add dynamic fields + for fieldName, fieldValue := range fields { + whereClause += fmt.Sprintf(" AND %s = $%d", fieldName, argIndex) + args = append(args, fieldValue) + argIndex++ + } + + query := fmt.Sprintf("SELECT COUNT(*) FROM %s WHERE %s", config.TableName, whereClause) + + var count int + err := dv.db.QueryRowContext(ctx, query, args...).Scan(&count) + if err != nil { + return fmt.Errorf("failed to check duplicate with custom fields: %w", err) + } + + if count > 0 { + return fmt.Errorf("duplicate entry found with the specified criteria") + } + + return nil +} + +// ValidateOncePerDay ensures only one submission per day for a given identifier +func (dv *DuplicateValidator) ValidateOncePerDay(ctx context.Context, tableName, idColumn, dateColumn string, identifier interface{}) error { + query := fmt.Sprintf(` + SELECT COUNT(*) + FROM %s + WHERE %s = $1 + AND DATE(%s) = CURRENT_DATE + `, tableName, idColumn, dateColumn) + + var count int + err := dv.db.QueryRowContext(ctx, query, identifier).Scan(&count) + if err != nil { + return fmt.Errorf("failed to check daily submission: %w", err) + } + + if count > 0 { + return fmt.Errorf("only one submission allowed per day for ID %v", identifier) + } + + return nil +} + +// GetLastSubmissionTime returns the last submission time for a given identifier +func (dv *DuplicateValidator) GetLastSubmissionTime(ctx context.Context, tableName, idColumn, dateColumn string, identifier interface{}) (*time.Time, error) { + query := fmt.Sprintf(` + SELECT %s + FROM %s + WHERE %s = $1 + ORDER BY %s DESC + LIMIT 1 + `, dateColumn, tableName, idColumn, dateColumn) + + var lastTime time.Time + err := dv.db.QueryRowContext(ctx, query, identifier).Scan(&lastTime) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil // No previous submission + } + return nil, fmt.Errorf("failed to get last submission time: %w", err) + } + + return &lastTime, nil +} + +// DefaultRetribusiConfig returns default configuration for retribusi validation +func DefaultRetribusiConfig() ValidationConfig { + return ValidationConfig{ + TableName: "data_retribusi", + IDColumn: "id", + StatusColumn: "status", + DateColumn: "date_created", + ActiveStatuses: []string{"active", "draft"}, + } +} diff --git a/pkg/logger/README.md b/pkg/logger/README.md new file mode 100644 index 0000000..918edda --- /dev/null +++ b/pkg/logger/README.md @@ -0,0 +1,356 @@ +# Structured Logger Package + +A comprehensive structured logging package for Go applications with support for different log levels, service-specific logging, request context, and JSON output formatting. + +## Features + +- **Structured Logging**: JSON and text format output with rich metadata +- **Multiple Log Levels**: DEBUG, INFO, WARN, ERROR, FATAL +- **Service-Specific Logging**: Dedicated loggers for different services +- **Request Context**: Request ID and correlation ID tracking +- **Performance Timing**: Built-in duration logging for operations +- **Gin Middleware**: Request logging middleware for HTTP requests +- **Environment Configuration**: Configurable via environment variables + +## Installation + +The logger is already integrated into the project. Import it using: + +```go +import "api-service/pkg/logger" +``` + +## Quick Start + +### Basic Usage + +```go +// Global functions (use default logger) +logger.Info("Application starting") +logger.Error("Something went wrong", map[string]interface{}{ + "error": err.Error(), + "code": "DB_CONNECTION_FAILED", +}) + +// Create a service-specific logger +authLogger := logger.ServiceLogger("auth-service") +authLogger.Info("User authenticated", map[string]interface{}{ + "user_id": "123", + "method": "oauth2", +}) +``` + +### Service-Specific Loggers + +```go +// Pre-defined service loggers +authLogger := logger.AuthServiceLogger() +bpjsLogger := logger.BPJSServiceLogger() +retribusiLogger := logger.RetribusiServiceLogger() +databaseLogger := logger.DatabaseServiceLogger() + +authLogger.Info("Authentication successful") +databaseLogger.Debug("Query executed", map[string]interface{}{ + "query": "SELECT * FROM users", + "time": "150ms", +}) +``` + +### Request Context Logging + +```go +// Add request context to logs +requestLogger := logger.Default(). + WithRequestID("req-123456"). + WithCorrelationID("corr-789012"). + WithField("user_id", "user-123") + +requestLogger.Info("Request processing started", map[string]interface{}{ + "endpoint": "/api/v1/data", + "method": "POST", +}) +``` + +### Performance Timing + +```go +// Time operations and log duration +start := time.Now() +// ... perform operation ... +logger.LogDuration(start, "Database query completed", map[string]interface{}{ + "query": "SELECT * FROM large_table", + "rows": 1000, + "database": "postgres", +}) +``` + +## Gin Middleware Integration + +### Add Request Logger Middleware + +In your routes setup: + +```go +import "api-service/pkg/logger" + +func RegisterRoutes(cfg *config.Config) *gin.Engine { + router := gin.New() + + // Add request logging middleware + router.Use(logger.RequestLoggerMiddleware(logger.Default())) + + // ... other middleware and routes + return router +} +``` + +### Access Logger in Handlers + +```go +func (h *MyHandler) MyEndpoint(c *gin.Context) { + // Get logger from context + logger := logger.GetLoggerFromContext(c) + + logger.Info("Endpoint called", map[string]interface{}{ + "user_agent": c.Request.UserAgent(), + "client_ip": c.ClientIP(), + }) + + // Get request IDs + requestID := logger.GetRequestIDFromContext(c) + correlationID := logger.GetCorrelationIDFromContext(c) +} +``` + +## Configuration + +### Environment Variables + +Set these environment variables to configure the logger: + +```bash +# Log level (DEBUG, INFO, WARN, ERROR, FATAL) +LOG_LEVEL=INFO + +# Output format (text or json) +LOG_FORMAT=text + +# Service name for logs +LOG_SERVICE=api-service + +# Enable JSON format +LOG_JSON=false +``` + +### Programmatic Configuration + +```go +// Create custom logger with specific configuration +cfg := logger.Config{ + Level: "DEBUG", + JSONFormat: true, + Service: "my-custom-service", +} + +customLogger := logger.NewFromConfig(cfg) + +// Or create manually +logger := logger.New("service-name", logger.DEBUG, true) +``` + +## Log Levels + +| Level | Description | Usage | +|-------|-------------|-------| +| DEBUG | Detailed debug information | Development and troubleshooting | +| INFO | General operational messages | Normal application behavior | +| WARN | Warning conditions | Something unexpected but not an error | +| ERROR | Error conditions | Operation failed but application continues | +| FATAL | Critical conditions | Application cannot continue | + +## Output Formats + +### Text Format (Default) +``` +2025-08-22T04:33:12+07:00 [INFO] auth-service: User authentication successful (handler/auth.go:45) [user_id=12345 method=oauth2] +``` + +### JSON Format +```json +{ + "timestamp": "2025-08-22T04:33:12+07:00", + "level": "INFO", + "service": "auth-service", + "message": "User authentication successful", + "file": "handler/auth.go", + "line": 45, + "request_id": "req-123456", + "correlation_id": "corr-789012", + "fields": { + "user_id": "12345", + "method": "oauth2" + } +} +``` + +## Best Practices + +### 1. Use Appropriate Log Levels +```go +// Good +logger.Debug("Detailed debug info") +logger.Info("User action completed") +logger.Warn("Rate limit approaching") +logger.Error("Database connection failed") + +// Avoid +logger.Info("Error connecting to database") // Use ERROR instead +``` + +### 2. Add Context to Logs +```go +// Instead of this: +logger.Error("Login failed") + +// Do this: +logger.Error("Login failed", map[string]interface{}{ + "username": username, + "reason": "invalid_credentials", + "attempts": loginAttempts, + "client_ip": clientIP, +}) +``` + +### 3. Use Service-Specific Loggers +```go +// Create once per service +var authLogger = logger.AuthServiceLogger() + +func LoginHandler(c *gin.Context) { + authLogger.Info("Login attempt", map[string]interface{}{ + "username": c.PostForm("username"), + }) +} +``` + +### 4. Measure Performance +```go +func ProcessData(data []byte) error { + start := time.Now() + defer func() { + logger.LogDuration(start, "Data processing completed", map[string]interface{}{ + "data_size": len(data), + "items": countItems(data), + }) + }() + + // ... processing logic ... +} +``` + +## Migration from Standard Log Package + +### Before (standard log) +```go +import "log" + +log.Printf("Error: %v", err) +log.Printf("User %s logged in", username) +``` + +### After (structured logger) +```go +import "api-service/pkg/logger" + +logger.Error("Operation failed", map[string]interface{}{ + "error": err.Error(), + "context": "user_login", +}) + +logger.Info("User logged in", map[string]interface{}{ + "username": username, + "method": "password", +}) +``` + +## Examples + +### Database Operations +```go +func (h *UserHandler) GetUser(c *gin.Context) { + logger := logger.GetLoggerFromContext(c) + start := time.Now() + + user, err := h.db.GetUser(c.Param("id")) + if err != nil { + logger.Error("Failed to get user", map[string]interface{}{ + "user_id": c.Param("id"), + "error": err.Error(), + }) + c.JSON(500, gin.H{"error": "Internal server error"}) + return + } + + logger.LogDuration(start, "User retrieved successfully", map[string]interface{}{ + "user_id": user.ID, + "query_time": time.Since(start).String(), + }) + + c.JSON(200, user) +} +``` + +### Authentication Service +```go +var authLogger = logger.AuthServiceLogger() + +func Authenticate(username, password string) (bool, error) { + authLogger.Debug("Authentication attempt", map[string]interface{}{ + "username": username, + }) + + // Authentication logic... + + if authenticated { + authLogger.Info("Authentication successful", map[string]interface{}{ + "username": username, + "method": "password", + }) + return true, nil + } + + authLogger.Warn("Authentication failed", map[string]interface{}{ + "username": username, + "reason": "invalid_credentials", + }) + return false, nil +} +``` + +## Troubleshooting + +### Common Issues + +1. **No logs appearing**: Check that log level is not set too high (e.g., ERROR when logging INFO) +2. **JSON format not working**: Ensure `LOG_JSON=true` or logger is created with `jsonFormat: true` +3. **Missing context**: Use `WithRequestID()` and `WithCorrelationID()` for request context + +### Debug Mode + +Enable debug logging for development: + +```bash +export LOG_LEVEL=DEBUG +export LOG_FORMAT=text +``` + +## Performance Considerations + +- Logger is designed to be lightweight and fast +- Context fields are only evaluated when the log level is enabled +- JSON marshaling only occurs when JSON format is enabled +- Consider log volume in production environments + +## License + +This logger package is part of the API Service project. diff --git a/pkg/logger/config.go b/pkg/logger/config.go new file mode 100644 index 0000000..68f69d1 --- /dev/null +++ b/pkg/logger/config.go @@ -0,0 +1,137 @@ +package logger + +import ( + "os" + "strconv" + "strings" +) + +// Config holds the configuration for the logger +type Config struct { + Level string `json:"level" default:"INFO"` + JSONFormat bool `json:"json_format" default:"false"` + Service string `json:"service" default:"api-service"` +} + +// DefaultConfig returns the default logger configuration +func DefaultConfig() Config { + return Config{ + Level: "INFO", + JSONFormat: false, + Service: "api-service", + } +} + +// LoadConfigFromEnv loads logger configuration from environment variables +func LoadConfigFromEnv() Config { + config := DefaultConfig() + + // Load log level from environment + if level := os.Getenv("LOG_LEVEL"); level != "" { + config.Level = strings.ToUpper(level) + } + + // Load JSON format from environment + if jsonFormat := os.Getenv("LOG_JSON_FORMAT"); jsonFormat != "" { + if parsed, err := strconv.ParseBool(jsonFormat); err == nil { + config.JSONFormat = parsed + } + } + + // Load service name from environment + if service := os.Getenv("LOG_SERVICE_NAME"); service != "" { + config.Service = service + } + + return config +} + +// Validate validates the logger configuration +func (c *Config) Validate() error { + // Validate log level + validLevels := map[string]bool{ + "DEBUG": true, + "INFO": true, + "WARN": true, + "ERROR": true, + "FATAL": true, + } + + if !validLevels[c.Level] { + c.Level = "INFO" // Default to INFO if invalid + } + + return nil +} + +// GetLogLevel returns the LogLevel from the configuration +func (c *Config) GetLogLevel() LogLevel { + switch strings.ToUpper(c.Level) { + case "DEBUG": + return DEBUG + case "WARN": + return WARN + case "ERROR": + return ERROR + case "FATAL": + return FATAL + default: + return INFO + } +} + +// CreateLoggerFromConfig creates a new logger instance from configuration +func CreateLoggerFromConfig(cfg Config) *Logger { + cfg.Validate() + return NewFromConfig(cfg) +} + +// CreateLoggerFromEnv creates a new logger instance from environment variables +func CreateLoggerFromEnv() *Logger { + cfg := LoadConfigFromEnv() + return CreateLoggerFromConfig(cfg) +} + +// Environment variable constants +const ( + EnvLogLevel = "LOG_LEVEL" + EnvLogJSONFormat = "LOG_JSON_FORMAT" + EnvLogService = "LOG_SERVICE_NAME" +) + +// Service-specific configuration helpers + +// AuthServiceConfig returns configuration for auth service +func AuthServiceConfig() Config { + cfg := LoadConfigFromEnv() + cfg.Service = "auth-service" + return cfg +} + +// BPJSServiceConfig returns configuration for BPJS service +func BPJSServiceConfig() Config { + cfg := LoadConfigFromEnv() + cfg.Service = "bpjs-service" + return cfg +} + +// RetribusiServiceConfig returns configuration for retribusi service +func RetribusiServiceConfig() Config { + cfg := LoadConfigFromEnv() + cfg.Service = "retribusi-service" + return cfg +} + +// DatabaseServiceConfig returns configuration for database service +func DatabaseServiceConfig() Config { + cfg := LoadConfigFromEnv() + cfg.Service = "database-service" + return cfg +} + +// MiddlewareServiceConfig returns configuration for middleware service +func MiddlewareServiceConfig() Config { + cfg := LoadConfigFromEnv() + cfg.Service = "middleware-service" + return cfg +} diff --git a/pkg/logger/context.go b/pkg/logger/context.go new file mode 100644 index 0000000..3eb52bf --- /dev/null +++ b/pkg/logger/context.go @@ -0,0 +1,142 @@ +package logger + +import ( + "context" + "time" +) + +// contextKey is a custom type for context keys to avoid collisions +type contextKey string + +const ( + loggerKey contextKey = "logger" + requestIDKey contextKey = "request_id" + correlationIDKey contextKey = "correlation_id" + serviceNameKey contextKey = "service_name" +) + +// ContextWithLogger creates a new context with the logger +func ContextWithLogger(ctx context.Context, logger *Logger) context.Context { + return context.WithValue(ctx, loggerKey, logger) +} + +// LoggerFromContext retrieves the logger from context +func LoggerFromContext(ctx context.Context) *Logger { + if logger, ok := ctx.Value(loggerKey).(*Logger); ok { + return logger + } + return globalLogger +} + +// ContextWithRequestID creates a new context with the request ID +func ContextWithRequestID(ctx context.Context, requestID string) context.Context { + return context.WithValue(ctx, requestIDKey, requestID) +} + +// RequestIDFromContext retrieves the request ID from context +func RequestIDFromContext(ctx context.Context) string { + if requestID, ok := ctx.Value(requestIDKey).(string); ok { + return requestID + } + return "" +} + +// ContextWithCorrelationID creates a new context with the correlation ID +func ContextWithCorrelationID(ctx context.Context, correlationID string) context.Context { + return context.WithValue(ctx, correlationIDKey, correlationID) +} + +// CorrelationIDFromContext retrieves the correlation ID from context +func CorrelationIDFromContext(ctx context.Context) string { + if correlationID, ok := ctx.Value(correlationIDKey).(string); ok { + return correlationID + } + return "" +} + +// ContextWithServiceName creates a new context with the service name +func ContextWithServiceName(ctx context.Context, serviceName string) context.Context { + return context.WithValue(ctx, serviceNameKey, serviceName) +} + +// ServiceNameFromContext retrieves the service name from context +func ServiceNameFromContext(ctx context.Context) string { + if serviceName, ok := ctx.Value(serviceNameKey).(string); ok { + return serviceName + } + return "" +} + +// WithContext returns a new logger with context values +func (l *Logger) WithContext(ctx context.Context) *Logger { + logger := l + + if requestID := RequestIDFromContext(ctx); requestID != "" { + logger = logger.WithRequestID(requestID) + } + + if correlationID := CorrelationIDFromContext(ctx); correlationID != "" { + logger = logger.WithCorrelationID(correlationID) + } + + if serviceName := ServiceNameFromContext(ctx); serviceName != "" { + logger = logger.WithService(serviceName) + } + + return logger +} + +// DebugCtx logs a debug message with context +func DebugCtx(ctx context.Context, msg string, fields ...map[string]interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Debug(msg, fields...) +} + +// DebugfCtx logs a formatted debug message with context +func DebugfCtx(ctx context.Context, format string, args ...interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Debugf(format, args...) +} + +// InfoCtx logs an info message with context +func InfoCtx(ctx context.Context, msg string, fields ...map[string]interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Info(msg, fields...) +} + +// InfofCtx logs a formatted info message with context +func InfofCtx(ctx context.Context, format string, args ...interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Infof(format, args...) +} + +// WarnCtx logs a warning message with context +func WarnCtx(ctx context.Context, msg string, fields ...map[string]interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Warn(msg, fields...) +} + +// WarnfCtx logs a formatted warning message with context +func WarnfCtx(ctx context.Context, format string, args ...interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Warnf(format, args...) +} + +// ErrorCtx logs an error message with context +func ErrorCtx(ctx context.Context, msg string, fields ...map[string]interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Error(msg, fields...) +} + +// ErrorfCtx logs a formatted error message with context +func ErrorfCtx(ctx context.Context, format string, args ...interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Errorf(format, args...) +} + +// FatalCtx logs a fatal message with context and exits the program +func FatalCtx(ctx context.Context, msg string, fields ...map[string]interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Fatal(msg, fields...) +} + +// FatalfCtx logs a formatted fatal message with context and exits the program +func FatalfCtx(ctx context.Context, format string, args ...interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Fatalf(format, args...) +} + +// LogDurationCtx logs the duration of an operation with context +func LogDurationCtx(ctx context.Context, start time.Time, operation string, fields ...map[string]interface{}) { + LoggerFromContext(ctx).WithContext(ctx).LogDuration(start, operation, fields...) +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 0000000..bcdd59e --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,616 @@ +package logger + +import ( + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "time" +) + +// LogLevel represents the severity level of a log message +type LogLevel int + +const ( + DEBUG LogLevel = iota + INFO + WARN + ERROR + FATAL +) + +var ( + levelStrings = map[LogLevel]string{ + DEBUG: "DEBUG", + INFO: "INFO", + WARN: "WARN", + ERROR: "ERROR", + FATAL: "FATAL", + } + + stringLevels = map[string]LogLevel{ + "DEBUG": DEBUG, + "INFO": INFO, + "WARN": WARN, + "ERROR": ERROR, + "FATAL": FATAL, + } +) + +// Logger represents a structured logger instance +type Logger struct { + serviceName string + level LogLevel + output *log.Logger + mu sync.Mutex + jsonFormat bool + + logDir string +} + +// LogEntry represents a structured log entry +type LogEntry struct { + Timestamp string `json:"timestamp"` + Level string `json:"level"` + Service string `json:"service"` + Message string `json:"message"` + RequestID string `json:"request_id,omitempty"` + CorrelationID string `json:"correlation_id,omitempty"` + File string `json:"file,omitempty"` + Line int `json:"line,omitempty"` + Duration string `json:"duration,omitempty"` + Fields map[string]interface{} `json:"fields,omitempty"` +} + +// New creates a new logger instance +func New(serviceName string, level LogLevel, jsonFormat bool, logDir ...string) *Logger { + // Tentukan direktori log berdasarkan prioritas: + // 1. Parameter logDir (jika disediakan) + // 2. Environment variable LOG_DIR (jika ada) + // 3. Default ke pkg/logger/data relatif terhadap root proyek + + var finalLogDir string + + // Cek apakah logDir disediakan sebagai parameter + if len(logDir) > 0 && logDir[0] != "" { + finalLogDir = logDir[0] + } else { + // Cek environment variable + if envLogDir := os.Getenv("LOG_DIR"); envLogDir != "" { + finalLogDir = envLogDir + } else { + // Default: dapatkan path relatif terhadap root proyek + // Dapatkan path executable + exePath, err := os.Executable() + if err != nil { + // Fallback ke current working directory jika gagal + finalLogDir = filepath.Join(".", "pkg", "logger", "data") + } else { + // Dapatkan direktori executable + exeDir := filepath.Dir(exePath) + + // Jika berjalan dengan go run, executable ada di temp directory + // Coba dapatkan path source code + if strings.Contains(exeDir, "go-build") || strings.Contains(exeDir, "tmp") { + // Gunakan runtime.Caller untuk mendapatkan path source + _, file, _, ok := runtime.Caller(0) + if ok { + // Dapatkan direktori source (2 level up dari pkg/logger) + sourceDir := filepath.Dir(file) + for i := 0; i < 3; i++ { // Naik 3 level ke root proyek + sourceDir = filepath.Dir(sourceDir) + } + finalLogDir = filepath.Join(sourceDir, "pkg", "logger", "data") + } else { + // Fallback + finalLogDir = filepath.Join(".", "pkg", "logger", "data") + } + } else { + // Untuk binary yang sudah dikompilasi, asumsikan struktur proyek + finalLogDir = filepath.Join(exeDir, "pkg", "logger", "data") + } + } + } + } + + // Konversi ke path absolut + absPath, err := filepath.Abs(finalLogDir) + if err == nil { + finalLogDir = absPath + } + + // Buat direktori jika belum ada + if err := os.MkdirAll(finalLogDir, 0755); err != nil { + // Fallback ke stdout jika gagal membuat direktori + fmt.Printf("Warning: Failed to create log directory %s: %v\n", finalLogDir, err) + return &Logger{ + serviceName: serviceName, + level: level, + output: log.New(os.Stdout, "", 0), + jsonFormat: jsonFormat, + logDir: "", // Kosongkan karena gagal + } + } + + return &Logger{ + serviceName: serviceName, + level: level, + output: log.New(os.Stdout, "", 0), + jsonFormat: jsonFormat, + logDir: finalLogDir, + } +} + +// NewFromConfig creates a new logger from configuration +func NewFromConfig(cfg Config) *Logger { + level := INFO + if l, exists := stringLevels[strings.ToUpper(cfg.Level)]; exists { + level = l + } + + return New(cfg.Service, level, cfg.JSONFormat) +} + +// Default creates a default logger instance +func Default() *Logger { + return New("api-service", INFO, false) +} + +// WithService returns a new logger with the specified service name +func (l *Logger) WithService(serviceName string) *Logger { + return &Logger{ + serviceName: serviceName, + level: l.level, + output: l.output, + jsonFormat: l.jsonFormat, + logDir: l.logDir, + } +} + +// SetLevel sets the log level for the logger +func (l *Logger) SetLevel(level LogLevel) { + l.mu.Lock() + defer l.mu.Unlock() + l.level = level +} + +// SetJSONFormat sets whether to output logs in JSON format +func (l *Logger) SetJSONFormat(jsonFormat bool) { + l.mu.Lock() + defer l.mu.Unlock() + l.jsonFormat = jsonFormat +} + +// Debug logs a debug message +func (l *Logger) Debug(msg string, fields ...map[string]interface{}) { + l.log(DEBUG, msg, nil, fields...) +} + +// Debugf logs a formatted debug message +func (l *Logger) Debugf(format string, args ...interface{}) { + l.log(DEBUG, fmt.Sprintf(format, args...), nil) +} + +// Info logs an info message +func (l *Logger) Info(msg string, fields ...map[string]interface{}) { + l.log(INFO, msg, nil, fields...) +} + +// Infof logs a formatted info message +func (l *Logger) Infof(format string, args ...interface{}) { + l.log(INFO, fmt.Sprintf(format, args...), nil) +} + +// Warn logs a warning message +func (l *Logger) Warn(msg string, fields ...map[string]interface{}) { + l.log(WARN, msg, nil, fields...) +} + +// Warnf logs a formatted warning message +func (l *Logger) Warnf(format string, args ...interface{}) { + l.log(WARN, fmt.Sprintf(format, args...), nil) +} + +// Error logs an error message +func (l *Logger) Error(msg string, fields ...map[string]interface{}) { + l.log(ERROR, msg, nil, fields...) +} + +// Errorf logs a formatted error message +func (l *Logger) Errorf(format string, args ...interface{}) { + l.log(ERROR, fmt.Sprintf(format, args...), nil) +} + +// Fatal logs a fatal message and exits the program +func (l *Logger) Fatal(msg string, fields ...map[string]interface{}) { + l.log(FATAL, msg, nil, fields...) + os.Exit(1) +} + +// Fatalf logs a formatted fatal message and exits the program +func (l *Logger) Fatalf(format string, args ...interface{}) { + l.log(FATAL, fmt.Sprintf(format, args...), nil) + os.Exit(1) +} + +// WithRequestID returns a new logger with the specified request ID +func (l *Logger) WithRequestID(requestID string) *Logger { + return l.withField("request_id", requestID) +} + +// WithCorrelationID returns a new logger with the specified correlation ID +func (l *Logger) WithCorrelationID(correlationID string) *Logger { + return l.withField("correlation_id", correlationID) +} + +// WithField returns a new logger with an additional field +func (l *Logger) WithField(key string, value interface{}) *Logger { + return l.withField(key, value) +} + +// WithFields returns a new logger with additional fields +func (l *Logger) WithFields(fields map[string]interface{}) *Logger { + return &Logger{ + serviceName: l.serviceName, + level: l.level, + output: l.output, + jsonFormat: l.jsonFormat, + logDir: l.logDir, + } +} + +// LogDuration logs the duration of an operation +func (l *Logger) LogDuration(start time.Time, operation string, fields ...map[string]interface{}) { + duration := time.Since(start) + l.Info(fmt.Sprintf("%s completed", operation), append(fields, map[string]interface{}{ + "duration": duration.String(), + "duration_ms": duration.Milliseconds(), + })...) +} + +// log is the internal logging method +func (l *Logger) log(level LogLevel, msg string, duration *time.Duration, fields ...map[string]interface{}) { + if level < l.level { + return + } + + // Get caller information + _, file, line, ok := runtime.Caller(3) // Adjust caller depth + var callerFile string + var callerLine int + if ok { + // Shorten file path + parts := strings.Split(file, "/") + if len(parts) > 2 { + callerFile = strings.Join(parts[len(parts)-2:], "/") + } else { + callerFile = file + } + callerLine = line + } + + // Merge all fields + mergedFields := make(map[string]interface{}) + for _, f := range fields { + for k, v := range f { + mergedFields[k] = v + } + } + + entry := LogEntry{ + Timestamp: time.Now().Format(time.RFC3339), + Level: levelStrings[level], + Service: l.serviceName, + Message: msg, + File: callerFile, + Line: callerLine, + Fields: mergedFields, + } + + if duration != nil { + entry.Duration = duration.String() + } + + if l.jsonFormat { + l.outputJSON(entry) + } else { + l.outputText(entry) + } + + if level == FATAL { + os.Exit(1) + } +} + +// outputJSON outputs the log entry in JSON format +func (l *Logger) outputJSON(entry LogEntry) { + jsonData, err := json.Marshal(entry) + if err != nil { + // Fallback to text output if JSON marshaling fails + l.outputText(entry) + return + } + l.output.Println(string(jsonData)) +} + +// outputText outputs the log entry in text format +func (l *Logger) outputText(entry LogEntry) { + timestamp := entry.Timestamp + level := entry.Level + service := entry.Service + message := entry.Message + + // Base log line + logLine := fmt.Sprintf("%s [%s] %s: %s", timestamp, level, service, message) + + // Add file and line if available + if entry.File != "" && entry.Line > 0 { + logLine += fmt.Sprintf(" (%s:%d)", entry.File, entry.Line) + } + + // Add request ID if available + if entry.RequestID != "" { + logLine += fmt.Sprintf(" [req:%s]", entry.RequestID) + } + + // Add correlation ID if available + if entry.CorrelationID != "" { + logLine += fmt.Sprintf(" [corr:%s]", entry.CorrelationID) + } + + // Add duration if available + if entry.Duration != "" { + logLine += fmt.Sprintf(" [dur:%s]", entry.Duration) + } + + // Add additional fields + if len(entry.Fields) > 0 { + fields := make([]string, 0, len(entry.Fields)) + for k, v := range entry.Fields { + fields = append(fields, fmt.Sprintf("%s=%v", k, v)) + } + logLine += " [" + strings.Join(fields, " ") + "]" + } + + l.output.Println(logLine) +} + +// withField creates a new logger with an additional field +func (l *Logger) withField(key string, value interface{}) *Logger { + return &Logger{ + serviceName: l.serviceName, + level: l.level, + output: l.output, + jsonFormat: l.jsonFormat, + logDir: l.logDir, + } +} + +// String returns the string representation of a log level +func (l LogLevel) String() string { + return levelStrings[l] +} + +// ParseLevel parses a string into a LogLevel +func ParseLevel(level string) (LogLevel, error) { + if l, exists := stringLevels[strings.ToUpper(level)]; exists { + return l, nil + } + return INFO, fmt.Errorf("invalid log level: %s", level) +} + +// Global logger instance +var globalLogger = Default() + +// SetGlobalLogger sets the global logger instance +func SetGlobalLogger(logger *Logger) { + globalLogger = logger +} + +// Global logging functions +func Debug(msg string, fields ...map[string]interface{}) { + globalLogger.Debug(msg, fields...) +} + +func Debugf(format string, args ...interface{}) { + globalLogger.Debugf(format, args...) +} + +func Info(msg string, fields ...map[string]interface{}) { + globalLogger.Info(msg, fields...) +} + +func Infof(format string, args ...interface{}) { + globalLogger.Infof(format, args...) +} + +func Warn(msg string, fields ...map[string]interface{}) { + globalLogger.Warn(msg, fields...) +} + +func Warnf(format string, args ...interface{}) { + globalLogger.Warnf(format, args...) +} + +func Error(msg string, fields ...map[string]interface{}) { + globalLogger.Error(msg, fields...) +} + +func Errorf(format string, args ...interface{}) { + globalLogger.Errorf(format, args...) +} + +func Fatal(msg string, fields ...map[string]interface{}) { + globalLogger.Fatal(msg, fields...) +} + +func Fatalf(format string, args ...interface{}) { + globalLogger.Fatalf(format, args...) +} + +// SaveLogText menyimpan log dalam format teks dengan pemisah | +func (l *Logger) SaveLogText(entry LogEntry) error { + // Format log dengan pemisah | + logLine := fmt.Sprintf("%s|%s|%s|%s|%s|%s|%s|%s:%d", + entry.Timestamp, + entry.Level, + entry.Service, + entry.Message, + entry.RequestID, + entry.CorrelationID, + entry.Duration, + entry.File, + entry.Line) + + // Tambahkan fields jika ada + if len(entry.Fields) > 0 { + fieldsStr := "" + for k, v := range entry.Fields { + fieldsStr += fmt.Sprintf("|%s=%v", k, v) + } + logLine += fieldsStr + } + logLine += "\n" + + // Buat direktori jika belum ada + if err := os.MkdirAll(l.logDir, 0755); err != nil { + return err + } + + // Tulis ke file dengan mutex lock untuk concurrency safety + l.mu.Lock() + defer l.mu.Unlock() + + filePath := filepath.Join(l.logDir, "logs.txt") + f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + + if _, err := f.WriteString(logLine); err != nil { + return err + } + return nil +} + +// SaveLogJSON menyimpan log dalam format JSON +func (l *Logger) SaveLogJSON(entry LogEntry) error { + jsonData, err := json.Marshal(entry) + if err != nil { + return err + } + + // Buat direktori jika belum ada + if err := os.MkdirAll(l.logDir, 0755); err != nil { + return err + } + + // Tulis ke file dengan mutex lock for concurrency safety + l.mu.Lock() + defer l.mu.Unlock() + + filePath := filepath.Join(l.logDir, "logs.json") + f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + + if _, err := f.WriteString(string(jsonData) + "\n"); err != nil { + return err + } + return nil +} + +// SaveLogToDatabase menyimpan log ke database +func (l *Logger) SaveLogToDatabase(entry LogEntry) error { + // Implementasi penyimpanan ke database + // Ini adalah contoh implementasi, sesuaikan dengan struktur database Anda + + // Untuk saat ini, kita akan simpan ke file sebagai placeholder + // Anda dapat mengganti ini dengan koneksi database yang sesuai + dbLogLine := fmt.Sprintf("DB_LOG: %s|%s|%s|%s\n", + entry.Timestamp, entry.Level, entry.Service, entry.Message) + + if err := os.MkdirAll(l.logDir, 0755); err != nil { + return err + } + + // Tulis ke file dengan mutex lock for concurrency safety + l.mu.Lock() + defer l.mu.Unlock() + + filePath := filepath.Join(l.logDir, "database_logs.txt") + f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + + if _, err := f.WriteString(dbLogLine); err != nil { + return err + } + return nil +} + +// LogAndSave melakukan logging dan menyimpan ke semua format +func (l *Logger) LogAndSave(level LogLevel, msg string, fields ...map[string]interface{}) { + // Panggil fungsi log biasa + l.log(level, msg, nil, fields...) + + // Dapatkan entry log yang baru dibuat + _, file, line, ok := runtime.Caller(2) + var callerFile string + var callerLine int + if ok { + parts := strings.Split(file, "/") + if len(parts) > 2 { + callerFile = strings.Join(parts[len(parts)-2:], "/") + } else { + callerFile = file + } + callerLine = line + } + + mergedFields := make(map[string]interface{}) + for _, f := range fields { + for k, v := range f { + mergedFields[k] = v + } + } + + entry := LogEntry{ + Timestamp: time.Now().Format(time.RFC3339), + Level: levelStrings[level], + Service: l.serviceName, + Message: msg, + File: callerFile, + Line: callerLine, + Fields: mergedFields, + } + + // Simpan ke semua format + go func() { + l.SaveLogText(entry) + l.SaveLogJSON(entry) + l.SaveLogToDatabase(entry) + }() +} + +// Global fungsi untuk menyimpan log +func SaveLogText(entry LogEntry) error { + return globalLogger.SaveLogText(entry) +} + +func SaveLogJSON(entry LogEntry) error { + return globalLogger.SaveLogJSON(entry) +} + +func SaveLogToDatabase(entry LogEntry) error { + return globalLogger.SaveLogToDatabase(entry) +} diff --git a/pkg/logger/middleware.go b/pkg/logger/middleware.go new file mode 100644 index 0000000..d063a83 --- /dev/null +++ b/pkg/logger/middleware.go @@ -0,0 +1,191 @@ +package logger + +import ( + "bytes" + "io" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// RequestLoggerMiddleware creates a Gin middleware for request logging +func RequestLoggerMiddleware(logger *Logger) gin.HandlerFunc { + return func(c *gin.Context) { + // Generate request ID if not present + requestID := c.GetHeader("X-Request-ID") + if requestID == "" { + requestID = uuid.New().String() + c.Header("X-Request-ID", requestID) + } + + // Get correlation ID + correlationID := c.GetHeader("X-Correlation-ID") + if correlationID == "" { + correlationID = uuid.New().String() + c.Header("X-Correlation-ID", correlationID) + } + + // Create request-scoped logger + reqLogger := logger. + WithRequestID(requestID). + WithCorrelationID(correlationID) + + // Store logger in context + c.Set("logger", reqLogger) + c.Set("request_id", requestID) + c.Set("correlation_id", correlationID) + + // Capture request body for logging if needed + var requestBody []byte + if c.Request.Body != nil && strings.HasPrefix(c.ContentType(), "application/json") { + requestBody, _ = io.ReadAll(c.Request.Body) + c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) + } + + // Start timer + start := time.Now() + + // Log request start + reqLogger.Info("Request started", map[string]interface{}{ + "method": c.Request.Method, + "path": c.Request.URL.Path, + "query": c.Request.URL.RawQuery, + "remote_addr": c.Request.RemoteAddr, + "user_agent": c.Request.UserAgent(), + "content_type": c.ContentType(), + "body_size": len(requestBody), + }) + + // Process request + c.Next() + + // Calculate duration + duration := time.Since(start) + + // Get response status + status := c.Writer.Status() + responseSize := c.Writer.Size() + + // Log level based on status code + var logLevel LogLevel + switch { + case status >= 500: + logLevel = ERROR + case status >= 400: + logLevel = WARN + default: + logLevel = INFO + } + + // Log request completion + fields := map[string]interface{}{ + "method": c.Request.Method, + "path": c.Request.URL.Path, + "status": status, + "duration": duration.String(), + "duration_ms": duration.Milliseconds(), + "response_size": responseSize, + "client_ip": c.ClientIP(), + "user_agent": c.Request.UserAgent(), + "content_type": c.ContentType(), + "content_length": c.Request.ContentLength, + } + + // Add query parameters if present + if c.Request.URL.RawQuery != "" { + fields["query"] = c.Request.URL.RawQuery + } + + // Add error information if present + if len(c.Errors) > 0 { + errors := make([]string, len(c.Errors)) + for i, err := range c.Errors { + errors[i] = err.Error() + } + fields["errors"] = errors + } + + reqLogger.log(logLevel, "Request completed", &duration, fields) + } +} + +// GetLoggerFromContext retrieves the logger from Gin context +func GetLoggerFromContext(c *gin.Context) *Logger { + if logger, exists := c.Get("logger"); exists { + if l, ok := logger.(*Logger); ok { + return l + } + } + return globalLogger +} + +// GetRequestIDFromContext retrieves the request ID from Gin context +func GetRequestIDFromContext(c *gin.Context) string { + if requestID, exists := c.Get("request_id"); exists { + if id, ok := requestID.(string); ok { + return id + } + } + return "" +} + +// GetCorrelationIDFromContext retrieves the correlation ID from Gin context +func GetCorrelationIDFromContext(c *gin.Context) string { + if correlationID, exists := c.Get("correlation_id"); exists { + if id, ok := correlationID.(string); ok { + return id + } + } + return "" +} + +// DatabaseLoggerMiddleware creates middleware for database operation logging +func DatabaseLoggerMiddleware(logger *Logger, serviceName string) gin.HandlerFunc { + return func(c *gin.Context) { + reqLogger := GetLoggerFromContext(c).WithService(serviceName) + c.Set("db_logger", reqLogger) + c.Next() + } +} + +// GetDBLoggerFromContext retrieves the database logger from Gin context +func GetDBLoggerFromContext(c *gin.Context) *Logger { + if logger, exists := c.Get("db_logger"); exists { + if l, ok := logger.(*Logger); ok { + return l + } + } + return GetLoggerFromContext(c) +} + +// ServiceLogger creates a service-specific logger +func ServiceLogger(serviceName string) *Logger { + return globalLogger.WithService(serviceName) +} + +// AuthServiceLogger returns a logger for auth service +func AuthServiceLogger() *Logger { + return ServiceLogger("auth-service") +} + +// BPJSServiceLogger returns a logger for BPJS service +func BPJSServiceLogger() *Logger { + return ServiceLogger("bpjs-service") +} + +// RetribusiServiceLogger returns a logger for retribusi service +func RetribusiServiceLogger() *Logger { + return ServiceLogger("retribusi-service") +} + +// DatabaseServiceLogger returns a logger for database operations +func DatabaseServiceLogger() *Logger { + return ServiceLogger("database-service") +} + +// MiddlewareServiceLogger returns a logger for middleware operations +func MiddlewareServiceLogger() *Logger { + return ServiceLogger("middleware-service") +} diff --git a/pkg/utils/etag.go b/pkg/utils/etag.go new file mode 100644 index 0000000..eeba954 --- /dev/null +++ b/pkg/utils/etag.go @@ -0,0 +1,54 @@ +package utils + +import ( + "fmt" + "strings" +) + +// ParseETag extracts the ETag value from HTTP ETag header +// Handles both strong ETags ("123") and weak ETags (W/"123") +func ParseETag(etag string) string { + if etag == "" { + return "" + } + + // Remove W/ prefix for weak ETags + if strings.HasPrefix(etag, "W/") { + etag = etag[2:] + } + + // Remove surrounding quotes + if len(etag) >= 2 && strings.HasPrefix(etag, "\"") && strings.HasSuffix(etag, "\"") { + etag = etag[1 : len(etag)-1] + } + + return etag +} + +// FormatETag formats a version ID into a proper HTTP ETag header value +func FormatETag(versionId string, weak bool) string { + if versionId == "" { + return "" + } + + if weak { + return fmt.Sprintf(`W/"%s"`, versionId) + } + + return fmt.Sprintf(`"%s"`, versionId) +} + +// IsValidETag validates if the given string is a valid ETag format +func IsValidETag(etag string) bool { + if etag == "" { + return false + } + + // Check for weak ETag format + if strings.HasPrefix(etag, "W/") { + etag = etag[2:] + } + + // Must be quoted + return len(etag) >= 2 && strings.HasPrefix(etag, "\"") && strings.HasSuffix(etag, "\"") +} diff --git a/pkg/validator/validator b/pkg/validator/validator new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/pkg/validator/validator @@ -0,0 +1 @@ + diff --git a/scripts/scripts b/scripts/scripts new file mode 100644 index 0000000..e69de29 diff --git a/tools/general/generate-handler.go b/tools/general/generate-handler.go new file mode 100644 index 0000000..bddbdcb --- /dev/null +++ b/tools/general/generate-handler.go @@ -0,0 +1,1740 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" +) + +// HandlerData contains template data for handler generation +type HandlerData struct { + Name string + NameLower string + NamePlural string + Category string // Untuk backward compatibility (bagian pertama) + DirPath string // Path direktori lengkap + ModuleName string + TableName string + HasGet bool + HasPost bool + HasPut bool + HasDelete bool + HasStats bool + HasDynamic bool + HasSearch bool + HasFilter bool + HasPagination bool + Timestamp string +} + +type PathInfo struct { + Category string + EntityName string + DirPath string + FilePath string +} + +// parseEntityPath - Logic parsing yang diperbaiki +func parseEntityPath(entityPath string) (*PathInfo, error) { + if strings.TrimSpace(entityPath) == "" { + return nil, fmt.Errorf("entity path cannot be empty") + } + var pathInfo PathInfo + parts := strings.Split(entityPath, "/") + // Validasi minimal 1 bagian (file saja) dan maksimal 4 + if len(parts) < 1 || len(parts) > 4 { + return nil, fmt.Errorf("invalid path format: use up to 4 levels like 'level1/level2/level3/entity'") + } + // Validasi bagian kosong + for i, part := range parts { + if strings.TrimSpace(part) == "" { + return nil, fmt.Errorf("empty path segment at position %d", i+1) + } + } + + pathInfo.EntityName = parts[len(parts)-1] + if len(parts) > 1 { + pathInfo.Category = parts[len(parts)-2] + pathInfo.DirPath = strings.Join(parts[:len(parts)-1], "/") + pathInfo.FilePath = pathInfo.DirPath + "/" + strings.ToLower(pathInfo.EntityName) + ".go" + } else { + pathInfo.Category = "models" + pathInfo.DirPath = "" + pathInfo.FilePath = strings.ToLower(pathInfo.EntityName) + ".go" + } + return &pathInfo, nil +} + +// validateMethods - Validasi method yang diinput +func validateMethods(methods []string) error { + validMethods := map[string]bool{ + "get": true, "post": true, "put": true, "delete": true, + "stats": true, "dynamic": true, "search": true, + } + + for _, method := range methods { + if !validMethods[strings.ToLower(method)] { + return fmt.Errorf("invalid method: %s. Valid methods: get, post, put, delete, stats, dynamic, search", method) + } + } + return nil +} + +// generateTableName - Generate table name berdasarkan path lengkap +func generateTableName(pathInfo *PathInfo) string { + entityLower := strings.ToLower(pathInfo.EntityName) + + if pathInfo.DirPath != "" { + // Replace "/" dengan "_" untuk table name + pathForTable := strings.ReplaceAll(pathInfo.DirPath, "/", "_") + return "data_" + pathForTable + "_" + entityLower + } + return "data_" + entityLower +} + +// createDirectories - Buat direktori sesuai struktur path +func createDirectories(pathInfo *PathInfo) (string, string, error) { + var handlerDir, modelDir string + + if pathInfo.DirPath != "" { + handlerDir = filepath.Join("internal", "handlers", pathInfo.DirPath) + modelDir = filepath.Join("internal", "models", pathInfo.DirPath) + } else { + handlerDir = filepath.Join("internal", "handlers") + modelDir = filepath.Join("internal", "models") + } + + // Create directories + for _, dir := range []string{handlerDir, modelDir} { + if err := os.MkdirAll(dir, 0755); err != nil { + return "", "", fmt.Errorf("failed to create directory %s: %v", dir, err) + } + } + + return handlerDir, modelDir, nil +} + +// setMethods - Set method flags berdasarkan input +func setMethods(data *HandlerData, methods []string) { + methodMap := map[string]*bool{ + "get": &data.HasGet, + "post": &data.HasPost, + "put": &data.HasPut, + "delete": &data.HasDelete, + "stats": &data.HasStats, + "dynamic": &data.HasDynamic, + "search": &data.HasSearch, + } + + for _, method := range methods { + if flag, exists := methodMap[strings.ToLower(method)]; exists { + *flag = true + } + } + + // Always add stats if we have get + if data.HasGet { + data.HasStats = true + } +} + +func main() { + // Validasi argument + if len(os.Args) < 2 { + fmt.Println("Usage: go run generate-handler.go [path/]entity [methods]") + fmt.Println("Examples:") + fmt.Println(" go run generate-handler.go product get post put delete") + fmt.Println(" go run generate-handler.go retribusi/tarif get post put delete dynamic search") + fmt.Println(" go run generate-handler.go product/category/subcategory/item get post") + fmt.Println("\nSupported methods: get, post, put, delete, stats, dynamic, search") + os.Exit(1) + } + + // Parse entity path + entityPath := strings.TrimSpace(os.Args[1]) + pathInfo, err := parseEntityPath(entityPath) + if err != nil { + fmt.Printf("โŒ Error parsing path: %v\n", err) + os.Exit(1) + } + + // Parse methods + var methods []string + if len(os.Args) > 2 { + methods = os.Args[2:] + } else { + // Default methods with advanced features + methods = []string{"get", "post", "put", "delete", "dynamic", "search"} + } + + // Validate methods + if err := validateMethods(methods); err != nil { + fmt.Printf("โŒ %v\n", err) + os.Exit(1) + } + + // Format names + entityName := strings.Title(pathInfo.EntityName) // PascalCase entity name + entityLower := strings.ToLower(pathInfo.EntityName) + entityPlural := entityLower + "s" + + // Generate table name + tableName := generateTableName(pathInfo) + + // Create HandlerData + data := HandlerData{ + Name: entityName, + NameLower: entityLower, + NamePlural: entityPlural, + Category: pathInfo.Category, + DirPath: pathInfo.DirPath, + ModuleName: "api-service", + TableName: tableName, + HasPagination: true, + HasFilter: true, + Timestamp: time.Now().Format("2006-01-02 15:04:05"), + } + + // Set methods + setMethods(&data, methods) + + // Create directories + handlerDir, modelDir, err := createDirectories(pathInfo) + if err != nil { + fmt.Printf("โŒ Error creating directories: %v\n", err) + os.Exit(1) + } + + // Generate files + generateHandlerFile(data, handlerDir) + generateModelFile(data, modelDir) + updateRoutesFile(data) + + // Success output + fmt.Printf("โœ… Successfully generated handler: %s\n", entityName) + if pathInfo.Category != "" { + fmt.Printf("๐Ÿ“ Category: %s\n", pathInfo.Category) + } + if pathInfo.DirPath != "" { + fmt.Printf("๐Ÿ“‚ Path: %s\n", pathInfo.DirPath) + } + fmt.Printf("๐Ÿ“„ Handler: %s\n", filepath.Join(handlerDir, entityLower+".go")) + fmt.Printf("๐Ÿ“„ Model: %s\n", filepath.Join(modelDir, entityLower+".go")) + fmt.Printf("๐Ÿ—„๏ธ Table: %s\n", tableName) + fmt.Printf("๐Ÿ› ๏ธ Methods: %s\n", strings.Join(methods, ", ")) +} + +// ================= HANDLER GENERATION ===================== +func generateHandlerFile(data HandlerData, handlerDir string) { + // var modelsImportPath string + // if data.Category != "" { + // modelsImportPath = data.ModuleName + "/internal/models/" + data.Category + // } else { + // modelsImportPath = data.ModuleName + "/internal/models" + // } + + // pakai strings.Builder biar lebih clean + var handlerContent strings.Builder + + // Header + handlerContent.WriteString("package handlers\n\n") + handlerContent.WriteString("import (\n") + handlerContent.WriteString(` "` + data.ModuleName + `/internal/config"` + "\n") + handlerContent.WriteString(` "` + data.ModuleName + `/internal/database"` + "\n") + handlerContent.WriteString(` models "` + data.ModuleName + `/internal/models"` + "\n") + if data.Category != "models" { + handlerContent.WriteString(` "` + data.ModuleName + `/internal/models/` + data.Category + `"` + "\n") + } + + // Conditional imports + if data.HasDynamic || data.HasSearch { + handlerContent.WriteString(` utils "` + data.ModuleName + `/internal/utils/filters"` + "\n") + } + + handlerContent.WriteString(` "` + data.ModuleName + `/internal/utils/validation"` + "\n") + handlerContent.WriteString(` "context"` + "\n") + handlerContent.WriteString(` "database/sql"` + "\n") + handlerContent.WriteString(` "fmt"` + "\n") + handlerContent.WriteString(` "log"` + "\n") + handlerContent.WriteString(` "net/http"` + "\n") + handlerContent.WriteString(` "strconv"` + "\n") + handlerContent.WriteString(` "strings"` + "\n") + handlerContent.WriteString(` "sync"` + "\n") + handlerContent.WriteString(` "time"` + "\n\n") + handlerContent.WriteString(` "github.com/gin-gonic/gin"` + "\n") + handlerContent.WriteString(` "github.com/go-playground/validator/v10"` + "\n") + handlerContent.WriteString(` "github.com/google/uuid"` + "\n") + handlerContent.WriteString(")\n\n") + + // Vars + handlerContent.WriteString("var (\n") + handlerContent.WriteString(" " + data.NameLower + "db database.Service\n") + handlerContent.WriteString(" " + data.NameLower + "once sync.Once\n") + handlerContent.WriteString(" " + data.NameLower + "validate *validator.Validate\n") + handlerContent.WriteString(")\n\n") + + // init func + handlerContent.WriteString("// Initialize the database connection and validator\n") + handlerContent.WriteString("func init() {\n") + handlerContent.WriteString(" " + data.NameLower + "once.Do(func() {\n") + handlerContent.WriteString(" " + data.NameLower + "db = database.New(config.LoadConfig())\n") + handlerContent.WriteString(" " + data.NameLower + "validate = validator.New()\n") + handlerContent.WriteString(" " + data.NameLower + "validate.RegisterValidation(\"" + data.NameLower + "_status\", validate" + data.Name + "Status)\n") + handlerContent.WriteString(" if " + data.NameLower + "db == nil {\n") + handlerContent.WriteString(" log.Fatal(\"Failed to initialize database connection\")\n") + handlerContent.WriteString(" }\n") + handlerContent.WriteString(" })\n") + handlerContent.WriteString("}\n\n") + + // Custom validation + handlerContent.WriteString("// Custom validation for " + data.NameLower + " status\n") + handlerContent.WriteString("func validate" + data.Name + "Status(fl validator.FieldLevel) bool {\n") + handlerContent.WriteString(" return models.IsValidStatus(fl.Field().String())\n") + handlerContent.WriteString("}\n\n") + + // Handler struct + handlerContent.WriteString("// " + data.Name + "Handler handles " + data.NameLower + " services\n") + handlerContent.WriteString("type " + data.Name + "Handler struct {\n") + handlerContent.WriteString(" db database.Service\n") + handlerContent.WriteString("}\n\n") + + // Constructor + handlerContent.WriteString("// New" + data.Name + "Handler creates a new " + data.Name + "Handler\n") + handlerContent.WriteString("func New" + data.Name + "Handler() *" + data.Name + "Handler {\n") + handlerContent.WriteString(" return &" + data.Name + "Handler{\n") + handlerContent.WriteString(" db: " + data.NameLower + "db,\n") + handlerContent.WriteString(" }\n") + handlerContent.WriteString("}\n") + + // Add optional methods + if data.HasGet { + handlerContent.WriteString(generateGetMethods(data)) + } + if data.HasDynamic { + handlerContent.WriteString(generateDynamicMethod(data)) + } + if data.HasSearch { + handlerContent.WriteString(generateSearchMethod(data)) + } + if data.HasPost { + handlerContent.WriteString(generateCreateMethod(data)) + } + if data.HasPut { + handlerContent.WriteString(generateUpdateMethod(data)) + } + if data.HasDelete { + handlerContent.WriteString(generateDeleteMethod(data)) + } + if data.HasStats { + handlerContent.WriteString(generateStatsMethod(data)) + } + + // Add helper methods + handlerContent.WriteString(generateHelperMethods(data)) + + // Write into file + writeFile(filepath.Join(handlerDir, data.NameLower+".go"), handlerContent.String()) +} + +func generateGetMethods(data HandlerData) string { + return ` + +// Get` + data.Name + ` godoc +// @Summary Get ` + data.NameLower + ` with pagination and optional aggregation +// @Description Returns a paginated list of ` + data.NamePlural + ` with optional summary statistics +// @Tags ` + data.Name + ` +// @Accept json +// @Produce json +// @Param limit query int false "Limit (max 100)" default(10) +// @Param offset query int false "Offset" default(0) +// @Param include_summary query bool false "Include aggregation summary" default(false) +// @Param status query string false "Filter by status" +// @Param search query string false "Search in multiple fields" +// @Success 200 {object} ` + data.Category + `.` + data.Name + `GetResponse "Success response" +// @Failure 400 {object} models.ErrorResponse "Bad request" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/` + data.NamePlural + ` [get] +func (h *` + data.Name + `Handler) Get` + data.Name + `(c *gin.Context) { + // Parse pagination parameters + limit, offset, err := h.parsePaginationParams(c) + if err != nil { + h.respondError(c, "Invalid pagination parameters", err, http.StatusBadRequest) + return + } + + // Parse filter parameters + filter := h.parseFilterParams(c) + includeAggregation := c.Query("include_summary") == "true" + + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + // Create context with timeout + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + // Execute concurrent operations + var ( + items []` + data.Category + `.` + data.Name + ` + total int + aggregateData *models.AggregateData + wg sync.WaitGroup + errChan = make(chan error, 3) + mu sync.Mutex + ) + + // Fetch total count + wg.Add(1) + go func() { + defer wg.Done() + if err := h.getTotalCount(ctx, dbConn, filter, &total); err != nil { + mu.Lock() + errChan <- fmt.Errorf("failed to get total count: %w", err) + mu.Unlock() + } + }() + + // Fetch main data + wg.Add(1) + go func() { + defer wg.Done() + result, err := h.fetch` + data.Name + `s(ctx, dbConn, filter, limit, offset) + mu.Lock() + if err != nil { + errChan <- fmt.Errorf("failed to fetch data: %w", err) + } else { + items = result + } + mu.Unlock() + }() + + // Fetch aggregation data if requested + if includeAggregation { + wg.Add(1) + go func() { + defer wg.Done() + result, err := h.getAggregateData(ctx, dbConn, filter) + mu.Lock() + if err != nil { + errChan <- fmt.Errorf("failed to get aggregate data: %w", err) + } else { + aggregateData = result + } + mu.Unlock() + }() + } + + // Wait for all goroutines + wg.Wait() + close(errChan) + + // Check for errors + for err := range errChan { + if err != nil { + h.logAndRespondError(c, "Data processing failed", err, http.StatusInternalServerError) + return + } + } + + // Build response + meta := h.calculateMeta(limit, offset, total) + response := ` + data.Category + `.` + data.Name + `GetResponse{ + Message: "Data ` + data.Category + ` berhasil diambil", + Data: items, + Meta: meta, + } + + if includeAggregation && aggregateData != nil { + response.Summary = aggregateData + } + + c.JSON(http.StatusOK, response) +} + +// Get` + data.Name + `ByID godoc +// @Summary Get ` + data.Name + ` by ID +// @Description Returns a single ` + data.NameLower + ` by ID +// @Tags ` + data.Name + ` +// @Accept json +// @Produce json +// @Param id path string true "` + data.Name + ` ID (UUID)" +// @Success 200 {object} ` + data.Category + `.` + data.Name + `GetByIDResponse "Success response" +// @Failure 400 {object} models.ErrorResponse "Invalid ID format" +// @Failure 404 {object} models.ErrorResponse "` + data.Name + ` not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/` + data.NameLower + `/{id} [get] +func (h *` + data.Name + `Handler) Get` + data.Name + `ByID(c *gin.Context) { + id := c.Param("id") + + // Validate UUID format + if _, err := uuid.Parse(id); err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + item, err := h.get` + data.Name + `ByID(ctx, dbConn, id) + if err != nil { + if err == sql.ErrNoRows { + h.respondError(c, "` + data.Name + ` not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to get ` + data.NameLower + `", err, http.StatusInternalServerError) + } + return + } + + response := ` + data.Category + `.` + data.Name + `GetByIDResponse{ + Message: "` + data.Category + ` details retrieved successfully", + Data: item, + } + + c.JSON(http.StatusOK, response) +}` +} + +func generateDynamicMethod(data HandlerData) string { + return ` + +// Get` + data.Name + `Dynamic godoc +// @Summary Get ` + data.NameLower + ` with dynamic filtering +// @Description Returns ` + data.NamePlural + ` with advanced dynamic filtering like Directus +// @Tags ` + data.Name + ` +// @Accept json +// @Produce json +// @Param fields query string false "Fields to select (e.g., fields=*.*)" +// @Param filter[column][operator] query string false "Dynamic filters (e.g., filter[name][_eq]=value)" +// @Param sort query string false "Sort fields (e.g., sort=date_created,-name)" +// @Param limit query int false "Limit" default(10) +// @Param offset query int false "Offset" default(0) +// @Success 200 {object} ` + data.Category + `.` + data.Name + `GetResponse "Success response" +// @Failure 400 {object} models.ErrorResponse "Bad request" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/` + data.NamePlural + `/dynamic [get] +func (h *` + data.Name + `Handler) Get` + data.Name + `Dynamic(c *gin.Context) { + // Parse query parameters + parser := utils.NewQueryParser().SetLimits(10, 100) + dynamicQuery, err := parser.ParseQuery(c.Request.URL.Query()) + if err != nil { + h.respondError(c, "Invalid query parameters", err, http.StatusBadRequest) + return + } + + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + // Create context with timeout + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + // Execute query with dynamic filtering + items, total, err := h.fetch` + data.Name + `sDynamic(ctx, dbConn, dynamicQuery) + if err != nil { + h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError) + return + } + + // Build response + meta := h.calculateMeta(dynamicQuery.Limit, dynamicQuery.Offset, total) + response := ` + data.Category + `.` + data.Name + `GetResponse{ + Message: "Data ` + data.Category + ` berhasil diambil", + Data: items, + Meta: meta, + } + + c.JSON(http.StatusOK, response) +}` +} + +func generateSearchMethod(data HandlerData) string { + return ` + +// Search` + data.Name + `Advanced provides advanced search capabilities +func (h *` + data.Name + `Handler) Search` + data.Name + `Advanced(c *gin.Context) { + // Parse complex search parameters + searchQuery := c.Query("q") + if searchQuery == "" { + h.respondError(c, "Search query is required", fmt.Errorf("empty search query"), http.StatusBadRequest) + return + } + + // Build dynamic query for search + query := utils.DynamicQuery{ + Fields: []string{"*"}, + Filters: []utils.FilterGroup{{ + Filters: []utils.DynamicFilter{ + { + Column: "status", + Operator: utils.OpNotEqual, + Value: "deleted", + }, + { + Column: "name", + Operator: utils.OpContains, + Value: searchQuery, + LogicOp: "OR", + }, + }, + LogicOp: "AND", + }}, + Sort: []utils.SortField{{ + Column: "date_created", + Order: "DESC", + }}, + Limit: 20, + Offset: 0, + } + + // Parse pagination if provided + if limit := c.Query("limit"); limit != "" { + if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 100 { + query.Limit = l + } + } + if offset := c.Query("offset"); offset != "" { + if o, err := strconv.Atoi(offset); err == nil && o >= 0 { + query.Offset = o + } + } + + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + // Execute search + items, total, err := h.fetch` + data.Name + `sDynamic(ctx, dbConn, query) + if err != nil { + h.logAndRespondError(c, "Search failed", err, http.StatusInternalServerError) + return + } + + // Build response + meta := h.calculateMeta(query.Limit, query.Offset, total) + response := ` + data.Category + `.` + data.Name + `GetResponse{ + Message: fmt.Sprintf("Search results for '%s'", searchQuery), + Data: items, + Meta: meta, + } + + c.JSON(http.StatusOK, response) +}` +} + +func generateCreateMethod(data HandlerData) string { + return ` + +// Create` + data.Name + ` godoc +// @Summary Create ` + data.NameLower + ` +// @Description Creates a new ` + data.NameLower + ` record +// @Tags ` + data.Name + ` +// @Accept json +// @Produce json +// @Param request body ` + data.Category + `.` + data.Name + `CreateRequest true "` + data.Name + ` creation request" +// @Success 201 {object} ` + data.Category + `.` + data.Name + `CreateResponse "` + data.Name + ` created successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/` + data.NamePlural + ` [post] +func (h *` + data.Name + `Handler) Create` + data.Name + `(c *gin.Context) { + var req ` + data.Category + `.` + data.Name + `CreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + + // Validate request + if err := ` + data.NameLower + `validate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + // Validate duplicate and daily submission + if err := h.validate` + data.Name + `Submission(ctx, dbConn, &req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + item, err := h.create` + data.Name + `(ctx, dbConn, &req) + if err != nil { + h.logAndRespondError(c, "Failed to create ` + data.NameLower + `", err, http.StatusInternalServerError) + return + } + + response := ` + data.Category + `.` + data.Name + `CreateResponse{ + Message: "` + data.Name + ` berhasil dibuat", + Data: item, + } + + c.JSON(http.StatusCreated, response) +}` +} + +func generateUpdateMethod(data HandlerData) string { + return ` + +// Update` + data.Name + ` godoc +// @Summary Update ` + data.NameLower + ` +// @Description Updates an existing ` + data.NameLower + ` record +// @Tags ` + data.Name + ` +// @Accept json +// @Produce json +// @Param id path string true "` + data.Name + ` ID (UUID)" +// @Param request body ` + data.Category + `.` + data.Name + `UpdateRequest true "` + data.Name + ` update request" +// @Success 200 {object} ` + data.Category + `.` + data.Name + `UpdateResponse "` + data.Name + ` updated successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 404 {object} models.ErrorResponse "` + data.Name + ` not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/` + data.NameLower + `/{id} [put] +func (h *` + data.Name + `Handler) Update` + data.Name + `(c *gin.Context) { + id := c.Param("id") + + // Validate UUID format + if _, err := uuid.Parse(id); err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } + + var req ` + data.Category + `.` + data.Name + `UpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + + // Set ID from path parameter + req.ID = id + + // Validate request + if err := ` + data.NameLower + `validate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + item, err := h.update` + data.Name + `(ctx, dbConn, &req) + if err != nil { + if err == sql.ErrNoRows { + h.respondError(c, "` + data.Name + ` not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to update ` + data.NameLower + `", err, http.StatusInternalServerError) + } + return + } + + response := ` + data.Category + `.` + data.Name + `UpdateResponse{ + Message: "` + data.Name + ` berhasil diperbarui", + Data: item, + } + + c.JSON(http.StatusOK, response) +}` +} + +func generateDeleteMethod(data HandlerData) string { + return ` + +// Delete` + data.Name + ` godoc +// @Summary Delete ` + data.NameLower + ` +// @Description Soft deletes a ` + data.NameLower + ` by setting status to 'deleted' +// @Tags ` + data.Name + ` +// @Accept json +// @Produce json +// @Param id path string true "` + data.Name + ` ID (UUID)" +// @Success 200 {object} ` + data.Category + `.` + data.Name + `DeleteResponse "` + data.Name + ` deleted successfully" +// @Failure 400 {object} models.ErrorResponse "Invalid ID format" +// @Failure 404 {object} models.ErrorResponse "` + data.Name + ` not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/` + data.NameLower + `/{id} [delete] +func (h *` + data.Name + `Handler) Delete` + data.Name + `(c *gin.Context) { + id := c.Param("id") + + // Validate UUID format + if _, err := uuid.Parse(id); err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + err = h.delete` + data.Name + `(ctx, dbConn, id) + if err != nil { + if err == sql.ErrNoRows { + h.respondError(c, "` + data.Name + ` not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to delete ` + data.NameLower + `", err, http.StatusInternalServerError) + } + return + } + + response := ` + data.Category + `.` + data.Name + `DeleteResponse{ + Message: "` + data.Name + ` berhasil dihapus", + ID: id, + } + + c.JSON(http.StatusOK, response) +}` +} + +func generateStatsMethod(data HandlerData) string { + return ` + +// Get` + data.Name + `Stats godoc +// @Summary Get ` + data.NameLower + ` statistics +// @Description Returns comprehensive statistics about ` + data.NameLower + ` data +// @Tags ` + data.Name + ` +// @Accept json +// @Produce json +// @Param status query string false "Filter statistics by status" +// @Success 200 {object} models.AggregateData "Statistics data" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/` + data.NamePlural + `/stats [get] +func (h *` + data.Name + `Handler) Get` + data.Name + `Stats(c *gin.Context) { + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + filter := h.parseFilterParams(c) + aggregateData, err := h.getAggregateData(ctx, dbConn, filter) + if err != nil { + h.logAndRespondError(c, "Failed to get statistics", err, http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Statistik ` + data.NameLower + ` berhasil diambil", + "data": aggregateData, + }) +}` +} + +func generateHelperMethods(data HandlerData) string { + helperMethods := ` + +// Database operations +func (h *` + data.Name + `Handler) get` + data.Name + `ByID(ctx context.Context, dbConn *sql.DB, id string) (*` + data.Category + `.` + data.Name + `, error) { + query := "SELECT id, status, sort, user_created, date_created, user_updated, date_updated, name FROM ` + data.TableName + ` WHERE id = $1 AND status != 'deleted'" + row := dbConn.QueryRowContext(ctx, query, id) + + var item ` + data.Category + `.` + data.Name + ` + err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name) + if err != nil { + return nil, err + } + + return &item, nil +} + +func (h *` + data.Name + `Handler) create` + data.Name + `(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `.` + data.Name + `CreateRequest) (*` + data.Category + `.` + data.Name + `, error) { + id := uuid.New().String() + now := time.Now() + + query := "INSERT INTO ` + data.TableName + ` (id, status, date_created, date_updated, name) VALUES ($1, $2, $3, $4, $5) RETURNING id, status, sort, user_created, date_created, user_updated, date_updated, name" + row := dbConn.QueryRowContext(ctx, query, id, req.Status, now, now, req.Name) + + var item ` + data.Category + `.` + data.Name + ` + err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name) + if err != nil { + return nil, fmt.Errorf("failed to create ` + data.NameLower + `: %w", err) + } + + return &item, nil +} + +func (h *` + data.Name + `Handler) update` + data.Name + `(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `.` + data.Name + `UpdateRequest) (*` + data.Category + `.` + data.Name + `, error) { + now := time.Now() + + query := "UPDATE ` + data.TableName + ` SET status = $2, date_updated = $3, name = $4 WHERE id = $1 AND status != 'deleted' RETURNING id, status, sort, user_created, date_created, user_updated, date_updated, name" + row := dbConn.QueryRowContext(ctx, query, req.ID, req.Status, now, req.Name) + + var item ` + data.Category + `.` + data.Name + ` + err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name) + if err != nil { + return nil, fmt.Errorf("failed to update ` + data.NameLower + `: %w", err) + } + + return &item, nil +} + +func (h *` + data.Name + `Handler) delete` + data.Name + `(ctx context.Context, dbConn *sql.DB, id string) error { + now := time.Now() + query := "UPDATE ` + data.TableName + ` SET status = 'deleted', date_updated = $2 WHERE id = $1 AND status != 'deleted'" + + result, err := dbConn.ExecContext(ctx, query, id, now) + if err != nil { + return fmt.Errorf("failed to delete ` + data.NameLower + `: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get affected rows: %w", err) + } + + if rowsAffected == 0 { + return sql.ErrNoRows + } + + return nil +} + +func (h *` + data.Name + `Handler) fetch` + data.Name + `s(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `.` + data.Name + `Filter, limit, offset int) ([]` + data.Category + `.` + data.Name + `, error) { + whereClause, args := h.buildWhereClause(filter) + query := fmt.Sprintf("SELECT id, status, sort, user_created, date_created, user_updated, date_updated, name FROM ` + data.TableName + ` WHERE %s ORDER BY date_created DESC NULLS LAST LIMIT $%d OFFSET $%d", whereClause, len(args)+1, len(args)+2) + args = append(args, limit, offset) + + rows, err := dbConn.QueryContext(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("fetch ` + data.NamePlural + ` query failed: %w", err) + } + defer rows.Close() + + items := make([]` + data.Category + `.` + data.Name + `, 0, limit) + for rows.Next() { + item, err := h.scan` + data.Name + `(rows) + if err != nil { + return nil, fmt.Errorf("scan ` + data.Name + ` failed: %w", err) + } + items = append(items, item) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows iteration error: %w", err) + } + + log.Printf("Successfully fetched %d ` + data.NamePlural + ` with filters applied", len(items)) + return items, nil +}` + + // Add dynamic fetch method if needed + if data.HasDynamic { + helperMethods += ` + +// fetchRetribusisDynamic executes dynamic query +func (h *` + data.Name + `Handler) fetch` + data.Name + `sDynamic(ctx context.Context, dbConn *sql.DB, query utils.DynamicQuery) ([]` + data.Category + `.` + data.Name + `, int, error) { + // Setup query builder + builder := utils.NewQueryBuilder("` + data.TableName + `"). + SetAllowedColumns([]string{ + "id", "status", "sort", "user_created", "date_created", + "user_updated", "date_updated", "name", + }) + + // Add default filter to exclude deleted records + query.Filters = append([]utils.FilterGroup{{ + Filters: []utils.DynamicFilter{{ + Column: "status", + Operator: utils.OpNotEqual, + Value: "deleted", + }}, + LogicOp: "AND", + }}, query.Filters...) + + // Execute concurrent queries + var ( + items [] ` + data.Category + `.` + data.Name + ` + total int + wg sync.WaitGroup + errChan = make(chan error, 2) + mu sync.Mutex + ) + + // Fetch total count + wg.Add(1) + go func() { + defer wg.Done() + countQuery := query + countQuery.Limit = 0 + countQuery.Offset = 0 + countSQL, countArgs, err := builder.BuildCountQuery(countQuery) + if err != nil { + errChan <- fmt.Errorf("failed to build count query: %w", err) + return + } + if err := dbConn.QueryRowContext(ctx, countSQL, countArgs...).Scan(&total); err != nil { + errChan <- fmt.Errorf("failed to get total count: %w", err) + return + } + }() + + // Fetch main data + wg.Add(1) + go func() { + defer wg.Done() + mainSQL, mainArgs, err := builder.BuildQuery(query) + if err != nil { + errChan <- fmt.Errorf("failed to build main query: %w", err) + return + } + + rows, err := dbConn.QueryContext(ctx, mainSQL, mainArgs...) + if err != nil { + errChan <- fmt.Errorf("failed to execute main query: %w", err) + return + } + defer rows.Close() + + var results []` + data.Category + `.` + data.Name + ` + for rows.Next() { + item, err := h.scan` + data.Name + `(rows) + if err != nil { + errChan <- fmt.Errorf("failed to scan ` + data.NameLower + `: %w", err) + return + } + results = append(results, item) + } + + if err := rows.Err(); err != nil { + errChan <- fmt.Errorf("rows iteration error: %w", err) + return + } + + mu.Lock() + items = results + mu.Unlock() + }() + + // Wait for all goroutines + wg.Wait() + close(errChan) + + // Check for errors + for err := range errChan { + if err != nil { + return nil, 0, err + } + } + + return items, total, nil +} +` + } + + helperMethods += ` +// Optimized scanning function +func (h *` + data.Name + `Handler) scan` + data.Name + `(rows *sql.Rows) (` + data.Category + `.` + data.Name + `, error) { + var item ` + data.Category + `.` + data.Name + ` + + // Scan into individual fields to handle nullable types properly + err := rows.Scan( + &item.ID, + &item.Status, + &item.Sort.Int32, &item.Sort.Valid, // models.NullableInt32 + &item.UserCreated.String, &item.UserCreated.Valid, // sql.NullString + &item.DateCreated.Time, &item.DateCreated.Valid, // sql.NullTime + &item.UserUpdated.String, &item.UserUpdated.Valid, // sql.NullString + &item.DateUpdated.Time, &item.DateUpdated.Valid, // sql.NullTime + &item.Name.String, &item.Name.Valid, // sql.NullString + ) + + return item, err +} + +func (h *` + data.Name + `Handler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `.` + data.Name + `Filter, total *int) error { + whereClause, args := h.buildWhereClause(filter) + countQuery := fmt.Sprintf("SELECT COUNT(*) FROM ` + data.TableName + ` WHERE %s", whereClause) + if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil { + return fmt.Errorf("total count query failed: %w", err) + } + return nil +} + +// Get comprehensive aggregate data dengan filter support +func (h *` + data.Name + `Handler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `.` + data.Name + `Filter) (*models.AggregateData, error) { + aggregate := &models.AggregateData{ + ByStatus: make(map[string]int), + } + + // Build where clause untuk filter + whereClause, args := h.buildWhereClause(filter) + + // Use concurrent execution untuk performance + var wg sync.WaitGroup + var mu sync.Mutex + errChan := make(chan error, 4) + + // 1. Count by status + wg.Add(1) + go func() { + defer wg.Done() + statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM ` + data.TableName + ` WHERE %s GROUP BY status ORDER BY status", whereClause) + + rows, err := dbConn.QueryContext(ctx, statusQuery, args...) + if err != nil { + errChan <- fmt.Errorf("status query failed: %w", err) + return + } + defer rows.Close() + + mu.Lock() + for rows.Next() { + var status string + var count int + if err := rows.Scan(&status, &count); err != nil { + mu.Unlock() + errChan <- fmt.Errorf("status scan failed: %w", err) + return + } + aggregate.ByStatus[status] = count + switch status { + case "active": + aggregate.TotalActive = count + case "draft": + aggregate.TotalDraft = count + case "inactive": + aggregate.TotalInactive = count + } + } + mu.Unlock() + + if err := rows.Err(); err != nil { + errChan <- fmt.Errorf("status iteration error: %w", err) + } + }() + + // 2. Get last updated time dan today statistics + wg.Add(1) + go func() { + defer wg.Done() + + // Last updated + lastUpdatedQuery := fmt.Sprintf("SELECT MAX(date_updated) FROM ` + data.TableName + ` WHERE %s AND date_updated IS NOT NULL", whereClause) + var lastUpdated sql.NullTime + if err := dbConn.QueryRowContext(ctx, lastUpdatedQuery, args...).Scan(&lastUpdated); err != nil { + errChan <- fmt.Errorf("last updated query failed: %w", err) + return + } + + // Today statistics + today := time.Now().Format("2006-01-02") + todayStatsQuery := fmt.Sprintf(` + "`" + ` + SELECT + SUM(CASE WHEN DATE(date_created) = $%d THEN 1 ELSE 0 END) as created_today, + SUM(CASE WHEN DATE(date_updated) = $%d AND DATE(date_created) != $%d THEN 1 ELSE 0 END) as updated_today + FROM ` + data.TableName + ` + WHERE %s` + "`" + `, len(args)+1, len(args)+1, len(args)+1, whereClause) + + todayArgs := append(args, today) + var createdToday, updatedToday int + if err := dbConn.QueryRowContext(ctx, todayStatsQuery, todayArgs...).Scan(&createdToday, &updatedToday); err != nil { + errChan <- fmt.Errorf("today stats query failed: %w", err) + return + } + + mu.Lock() + if lastUpdated.Valid { + aggregate.LastUpdated = &lastUpdated.Time + } + aggregate.CreatedToday = createdToday + aggregate.UpdatedToday = updatedToday + mu.Unlock() + }() + + // Wait for all goroutines + wg.Wait() + close(errChan) + + // Check for errors + for err := range errChan { + if err != nil { + return nil, err + } + } + + return aggregate, nil +} + +// Enhanced error handling +func (h *` + data.Name + `Handler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) { + log.Printf("[ERROR] %s: %v", message, err) + h.respondError(c, message, err, statusCode) +} + +func (h *` + data.Name + `Handler) respondError(c *gin.Context, message string, err error, statusCode int) { + errorMessage := message + if gin.Mode() == gin.ReleaseMode { + errorMessage = "Internal server error" + } + + c.JSON(statusCode, models.ErrorResponse{ + Error: errorMessage, + Code: statusCode, + Message: err.Error(), + Timestamp: time.Now(), + }) +} + +// Parse pagination parameters dengan validation yang lebih ketat +func (h *` + data.Name + `Handler) parsePaginationParams(c *gin.Context) (int, int, error) { + limit := 10 // Default limit + offset := 0 // Default offset + + if limitStr := c.Query("limit"); limitStr != "" { + parsedLimit, err := strconv.Atoi(limitStr) + if err != nil { + return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr) + } + if parsedLimit <= 0 { + return 0, 0, fmt.Errorf("limit must be greater than 0") + } + if parsedLimit > 100 { + return 0, 0, fmt.Errorf("limit cannot exceed 100") + } + limit = parsedLimit + } + + if offsetStr := c.Query("offset"); offsetStr != "" { + parsedOffset, err := strconv.Atoi(offsetStr) + if err != nil { + return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr) + } + if parsedOffset < 0 { + return 0, 0, fmt.Errorf("offset cannot be negative") + } + offset = parsedOffset + } + + log.Printf("Pagination - Limit: %d, Offset: %d", limit, offset) + return limit, offset, nil +} + +func (h *` + data.Name + `Handler) parseFilterParams(c *gin.Context) ` + data.Category + `.` + data.Name + `Filter { + filter := ` + data.Category + `.` + data.Name + `Filter{} + + if status := c.Query("status"); status != "" { + if models.IsValidStatus(status) { + filter.Status = &status + } + } + + if search := c.Query("search"); search != "" { + filter.Search = &search + } + + // Parse date filters + if dateFromStr := c.Query("date_from"); dateFromStr != "" { + if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil { + filter.DateFrom = &dateFrom + } + } + + if dateToStr := c.Query("date_to"); dateToStr != "" { + if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil { + filter.DateTo = &dateTo + } + } + + return filter +} + +// Build WHERE clause dengan filter parameters +func (h *` + data.Name + `Handler) buildWhereClause(filter ` + data.Category + `.` + data.Name + `Filter) (string, []interface{}) { + conditions := []string{"status != 'deleted'"} + args := []interface{}{} + paramCount := 1 + + if filter.Status != nil { + conditions = append(conditions, fmt.Sprintf("status = $%d", paramCount)) + args = append(args, *filter.Status) + paramCount++ + } + + if filter.Search != nil { + searchCondition := fmt.Sprintf("name ILIKE $%d", paramCount) + conditions = append(conditions, searchCondition) + searchTerm := "%" + *filter.Search + "%" + args = append(args, searchTerm) + paramCount++ + } + + if filter.DateFrom != nil { + conditions = append(conditions, fmt.Sprintf("date_created >= $%d", paramCount)) + args = append(args, *filter.DateFrom) + paramCount++ + } + + if filter.DateTo != nil { + conditions = append(conditions, fmt.Sprintf("date_created <= $%d", paramCount)) + args = append(args, filter.DateTo.Add(24*time.Hour-time.Nanosecond)) + paramCount++ + } + + return strings.Join(conditions, " AND "), args +} + +func (h *` + data.Name + `Handler) calculateMeta(limit, offset, total int) models.MetaResponse { + totalPages := 0 + currentPage := 1 + if limit > 0 { + totalPages = (total + limit - 1) / limit // Ceiling division + currentPage = (offset / limit) + 1 + } + + return models.MetaResponse{ + Limit: limit, + Offset: offset, + Total: total, + TotalPages: totalPages, + CurrentPage: currentPage, + HasNext: offset+limit < total, + HasPrev: offset > 0, + } +} + +// validate` + data.Name + `Submission performs validation for duplicate entries and daily submission limits +func (h *` + data.Name + `Handler) validate` + data.Name + `Submission(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `.` + data.Name + `CreateRequest) error { + // Import the validation utility + validator := validation.NewDuplicateValidator(dbConn) + + // Use default configuration + config := validation.ValidationConfig{ + TableName: "` + data.TableName + `", + IDColumn: "id", + StatusColumn: "status", + DateColumn: "date_created", + ActiveStatuses: []string{"active", "draft"}, + } + + // Validate duplicate entries with active status for today + err := validator.ValidateDuplicate(ctx, config, "dummy_id") + if err != nil { + return fmt.Errorf("validation failed: %w", err) + } + + // Validate once per day submission + err = validator.ValidateOncePerDay(ctx, "` + data.TableName + `", "id", "date_created", "daily_limit") + if err != nil { + return fmt.Errorf("daily submission limit exceeded: %w", err) + } + + return nil +} + +// Example usage of the validation utility with custom configuration +func (h *` + data.Name + `Handler) validateWithCustomConfig(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `.` + data.Name + `CreateRequest) error { + // Create validator instance + validator := validation.NewDuplicateValidator(dbConn) + + // Use custom configuration + config := validation.ValidationConfig{ + TableName: "` + data.TableName + `", + IDColumn: "id", + StatusColumn: "status", + DateColumn: "date_created", + ActiveStatuses: []string{"active", "draft"}, + AdditionalFields: map[string]interface{}{ + "name": req.Name, + }, + } + + // Validate with custom fields + fields := map[string]interface{}{ + "name": *req.Name, + } + + err := validator.ValidateDuplicateWithCustomFields(ctx, config, fields) + if err != nil { + return fmt.Errorf("custom validation failed: %w", err) + } + + return nil +} + +// GetLastSubmissionTime example +func (h *` + data.Name + `Handler) getLastSubmissionTimeExample(ctx context.Context, dbConn *sql.DB, identifier string) (*time.Time, error) { + validator := validation.NewDuplicateValidator(dbConn) + return validator.GetLastSubmissionTime(ctx, "` + data.TableName + `", "id", "date_created", identifier) +}` + + return helperMethods +} + +// Keep existing functions for model generation and routes... +// (The remaining functions stay the same as in the original file) + +// ================= MODEL GENERATION ===================== +func generateModelFile(data HandlerData, modelDir string) { + // Tentukan import block + var importBlock, nullablePrefix string + + if data.Category == "models" { + importBlock = `import ( + "database/sql" + "encoding/json" + "time" +) +` + } else { + nullablePrefix = "models." + importBlock = `import ( + "` + data.ModuleName + `/internal/models" + "database/sql" + "encoding/json" + "time" +) +` + } + + modelContent := `package ` + data.Category + ` + +` + importBlock + ` + +// ` + data.Name + ` represents the data structure for the ` + data.NameLower + ` table +// with proper null handling and optimized JSON marshaling +type ` + data.Name + ` struct { + ID string ` + "`json:\"id\" db:\"id\"`" + ` + Status string ` + "`json:\"status\" db:\"status\"`" + ` + Sort ` + nullablePrefix + "NullableInt32 `json:\"sort,omitempty\" db:\"sort\"`" + ` + UserCreated sql.NullString ` + "`json:\"user_created,omitempty\" db:\"user_created\"`" + ` + DateCreated sql.NullTime ` + "`json:\"date_created,omitempty\" db:\"date_created\"`" + ` + UserUpdated sql.NullString ` + "`json:\"user_updated,omitempty\" db:\"user_updated\"`" + ` + DateUpdated sql.NullTime ` + "`json:\"date_updated,omitempty\" db:\"date_updated\"`" + ` + Name sql.NullString ` + "`json:\"name,omitempty\" db:\"name\"`" + ` +} + +// Custom JSON marshaling untuk ` + data.Name + ` agar NULL values tidak muncul di response +func (r ` + data.Name + `) MarshalJSON() ([]byte, error) { + type Alias ` + data.Name + ` + aux := &struct { + Sort *int ` + "`json:\"sort,omitempty\"`" + ` + UserCreated *string ` + "`json:\"user_created,omitempty\"`" + ` + DateCreated *time.Time ` + "`json:\"date_created,omitempty\"`" + ` + UserUpdated *string ` + "`json:\"user_updated,omitempty\"`" + ` + DateUpdated *time.Time ` + "`json:\"date_updated,omitempty\"`" + ` + Name *string ` + "`json:\"name,omitempty\"`" + ` + *Alias + }{ + Alias: (*Alias)(&r), + } + + if r.Sort.Valid { + sort := int(r.Sort.Int32) + aux.Sort = &sort + } + if r.UserCreated.Valid { + aux.UserCreated = &r.UserCreated.String + } + if r.DateCreated.Valid { + aux.DateCreated = &r.DateCreated.Time + } + if r.UserUpdated.Valid { + aux.UserUpdated = &r.UserUpdated.String + } + if r.DateUpdated.Valid { + aux.DateUpdated = &r.DateUpdated.Time + } + if r.Name.Valid { + aux.Name = &r.Name.String + } + return json.Marshal(aux) +} + +// Helper methods untuk mendapatkan nilai yang aman +func (r *` + data.Name + `) GetName() string { + if r.Name.Valid { + return r.Name.String + } + return "" +} +` + + // Add request/response structs based on enabled methods + if data.HasGet { + modelContent += ` + +// Response struct untuk GET by ID +type ` + data.Name + `GetByIDResponse struct { + Message string ` + "`json:\"message\"`" + ` + Data *` + data.Name + ` ` + "`json:\"data\"`" + ` +} + +// Enhanced GET response dengan pagination dan aggregation +type ` + data.Name + `GetResponse struct { + Message string ` + "`json:\"message\"`" + ` + Data []` + data.Name + ` ` + "`json:\"data\"`" + ` + Meta ` + nullablePrefix + "MetaResponse `json:\"meta\"`" + ` + Summary *` + nullablePrefix + "AggregateData `json:\"summary,omitempty\"`" + ` +} +` + } + if data.HasPost { + modelContent += ` + +// Request struct untuk create +type ` + data.Name + `CreateRequest struct { + Status string ` + "`json:\"status\" validate:\"required,oneof=draft active inactive\"`" + ` + Name *string ` + "`json:\"name,omitempty\" validate:\"omitempty,min=1,max=255\"`" + ` +} + +// Response struct untuk create +type ` + data.Name + `CreateResponse struct { + Message string ` + "`json:\"message\"`" + ` + Data *` + data.Name + ` ` + "`json:\"data\"`" + ` +} +` + } + if data.HasPut { + modelContent += ` + +// Update request +type ` + data.Name + `UpdateRequest struct { + ID string ` + "`json:\"-\" validate:\"required,uuid4\"`" + ` + Status string ` + "`json:\"status\" validate:\"required,oneof=draft active inactive\"`" + ` + Name *string ` + "`json:\"name,omitempty\" validate:\"omitempty,min=1,max=255\"`" + ` +} + +// Response struct untuk update +type ` + data.Name + `UpdateResponse struct { + Message string ` + "`json:\"message\"`" + ` + Data *` + data.Name + ` ` + "`json:\"data\"`" + ` +} +` + } + if data.HasDelete { + modelContent += ` + +// Response struct untuk delete +type ` + data.Name + `DeleteResponse struct { + Message string ` + "`json:\"message\"`" + ` + ID string ` + "`json:\"id\"`" + ` +} +` + } + // Add filter struct + modelContent += ` + +// Filter struct untuk query parameters +type ` + data.Name + `Filter struct { + Status *string ` + "`json:\"status,omitempty\" form:\"status\"`" + ` + Search *string ` + "`json:\"search,omitempty\" form:\"search\"`" + ` + DateFrom *time.Time ` + "`json:\"date_from,omitempty\" form:\"date_from\"`" + ` + DateTo *time.Time ` + "`json:\"date_to,omitempty\" form:\"date_to\"`" + ` +} +` + writeFile(filepath.Join(modelDir, data.NameLower+".go"), modelContent) +} + +// ================= ROUTES GENERATION ===================== +func updateRoutesFile(data HandlerData) { + routesFile := "internal/routes/v1/routes.go" + content, err := os.ReadFile(routesFile) + if err != nil { + fmt.Printf("โš ๏ธ Could not read routes.go: %v\n", err) + fmt.Printf("๐Ÿ“ Please manually add these routes to your routes.go file:\n") + printRoutesSample(data) + return + } + + routesContent := string(content) + + // Build import path + var importPath, importAlias string + if data.Category != "models" { + importPath = fmt.Sprintf("%s/internal/handlers/"+data.Category, data.ModuleName) + importAlias = data.Category + data.Name + "Handlers" + } else { + importPath = fmt.Sprintf("%s/internal/handlers", data.ModuleName) + importAlias = data.NameLower + "Handlers" + } + + // Add import + importPattern := fmt.Sprintf("%s \"%s\"", importAlias, importPath) + if !strings.Contains(routesContent, importPattern) { + importToAdd := fmt.Sprintf("\t%s \"%s\"", importAlias, importPath) + if strings.Contains(routesContent, "import (") { + routesContent = strings.Replace(routesContent, "import (", + "import (\n"+importToAdd, 1) + } + } + + // Build new routes in protected group format + newRoutes := generateProtectedRouteBlock(data) + + // Insert above protected routes marker + insertMarker := "// ============= PUBLISHED ROUTES ===============================================" + if strings.Contains(routesContent, insertMarker) { + if !strings.Contains(routesContent, fmt.Sprintf("New%sHandler", data.Name)) { + // Insert before the marker + routesContent = strings.Replace(routesContent, insertMarker, + newRoutes+"\n\t"+insertMarker, 1) + } else { + fmt.Printf("โœ… Routes for %s already exist, skipping...\n", data.Name) + return + } + } else { + // Fallback: insert at end of setupV1Routes function + setupFuncEnd := "\treturn r" + if strings.Contains(routesContent, setupFuncEnd) { + routesContent = strings.Replace(routesContent, setupFuncEnd, + newRoutes+"\n\n\t"+setupFuncEnd, 1) + } + } + + if err := os.WriteFile(routesFile, []byte(routesContent), 0644); err != nil { + fmt.Printf("Error writing routes.go: %v\n", err) + return + } + + fmt.Printf("โœ… Updated routes.go with %s endpoints\n", data.Name) +} + +func generateProtectedRouteBlock(data HandlerData) string { + // fmt.Printf("๐Ÿ“ Group Part: %s\n", groupPath) + var sb strings.Builder + var importPath, groupPath string + if data.Category != "models" { + importPath = data.Category + data.Name + groupPath = strings.ToLower(data.Category) + "/" + data.NameLower + } else { + importPath = data.NameLower + groupPath = data.NameLower + } + // Komentar dan deklarasi handler & grup + sb.WriteString("// ") + sb.WriteString(data.Name) + sb.WriteString(" endpoints\n") + sb.WriteString(" ") + sb.WriteString(importPath) + sb.WriteString("Handler := ") + sb.WriteString(importPath) + sb.WriteString("Handlers.New") + sb.WriteString(data.Name) + sb.WriteString("Handler()\n ") + sb.WriteString(importPath) + + sb.WriteString("Group := v1.Group(\"/") + sb.WriteString(groupPath) + sb.WriteString("\")\n {\n ") + sb.WriteString(importPath) + sb.WriteString("Group.GET(\"\", ") + sb.WriteString(importPath) + sb.WriteString("Handler.Get") + sb.WriteString(data.Name) + sb.WriteString(")\n") + + if data.HasDynamic { + sb.WriteString(" ") + sb.WriteString(importPath) + sb.WriteString("Group.GET(\"/dynamic\", ") + sb.WriteString(importPath) + sb.WriteString("Handler.Get") + sb.WriteString(data.Name) + sb.WriteString("Dynamic) // Route baru\n") + } + if data.HasSearch { + sb.WriteString(" ") + sb.WriteString(importPath) + sb.WriteString("Group.GET(\"/search\", ") + sb.WriteString(importPath) + sb.WriteString("Handler.Search") + sb.WriteString(data.Name) + sb.WriteString("Advanced) // Route pencarian\n") + } + sb.WriteString(" ") + sb.WriteString(importPath) + sb.WriteString("Group.GET(\"/:id\", ") + sb.WriteString(importPath) + sb.WriteString("Handler.Get") + sb.WriteString(data.Name) + sb.WriteString("ByID)\n") + + if data.HasPost { + sb.WriteString(" ") + sb.WriteString(importPath) + sb.WriteString("Group.POST(\"\", ") + sb.WriteString(importPath) + sb.WriteString("Handler.Create") + sb.WriteString(data.Name) + sb.WriteString(")\n") + } + if data.HasPut { + sb.WriteString(" ") + sb.WriteString(importPath) + sb.WriteString("Group.PUT(\"/:id\", ") + sb.WriteString(importPath) + sb.WriteString("Handler.Update") + sb.WriteString(data.Name) + sb.WriteString(")\n") + } + if data.HasDelete { + sb.WriteString(" ") + sb.WriteString(importPath) + sb.WriteString("Group.DELETE(\"/:id\", ") + sb.WriteString(importPath) + sb.WriteString("Handler.Delete") + sb.WriteString(data.Name) + sb.WriteString(")\n") + } + if data.HasStats { + sb.WriteString(" ") + sb.WriteString(importPath) + sb.WriteString("Group.GET(\"/stats\", ") + sb.WriteString(importPath) + sb.WriteString("Handler.Get") + sb.WriteString(data.Name) + sb.WriteString("Stats)\n") + } + sb.WriteString(" }\n") + return sb.String() +} + +func printRoutesSample(data HandlerData) { + fmt.Print(generateProtectedRouteBlock(data)) + fmt.Println() +} + +// ================= UTILITY FUNCTIONS ===================== +func writeFile(filename, content string) { + if err := os.WriteFile(filename, []byte(content), 0644); err != nil { + fmt.Printf("โŒ Error creating file %s: %v\n", filename, err) + return + } + fmt.Printf("โœ… Generated: %s\n", filename) +}