refactor(API): ✨ Relspect integration
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
- Be precise, give short answers unless I ask for more detail
|
||||||
|
- Ask me when you are about to make lots of changes
|
||||||
|
- Make sure to update readme or any documentation
|
||||||
|
|||||||
225
CHECKPOINT_PHASE2_BACKEND.md
Normal file
225
CHECKPOINT_PHASE2_BACKEND.md
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
# Phase 2 Backend Checkpoint - COMPLETE ✅
|
||||||
|
|
||||||
|
**Date**: 2026-02-05
|
||||||
|
**Status**: Phase 2 Backend 100% Complete
|
||||||
|
|
||||||
|
## What Was Completed
|
||||||
|
|
||||||
|
### 1. BUN ORM Migration (COMPLETE)
|
||||||
|
- ✅ Converted all storage layer from GORM to BUN
|
||||||
|
- ✅ Generated 7 BUN models from DBML schema using `relspec` tool
|
||||||
|
- ✅ Updated `pkg/storage/db.go` with BUN connection handling
|
||||||
|
- ✅ Converted `pkg/storage/repository.go` to use BUN queries
|
||||||
|
- ✅ Updated seed data to use BUN models
|
||||||
|
|
||||||
|
**Generated Models** (`pkg/models/`):
|
||||||
|
- `sql_public_users.go` → `ModelPublicUser`
|
||||||
|
- `sql_public_api_keys.go` → `ModelPublicAPIKey`
|
||||||
|
- `sql_public_hooks.go` → `ModelPublicHook`
|
||||||
|
- `sql_public_whatsapp_accounts.go` → `ModelPublicWhatsappAccount`
|
||||||
|
- `sql_public_event_logs.go` → `ModelPublicEventLog`
|
||||||
|
- `sql_public_sessions.go` → `ModelPublicSession`
|
||||||
|
- `sql_public_message_cache.go` → `ModelPublicMessageCache`
|
||||||
|
|
||||||
|
### 2. ResolveSpec API Integration (COMPLETE)
|
||||||
|
- ✅ Created `pkg/api/server.go` with ResolveSpec framework
|
||||||
|
- ✅ Created `pkg/api/security.go` with JWT authentication
|
||||||
|
- ✅ Auto-generates REST CRUD endpoints for all models
|
||||||
|
- ✅ Implements row-level security (multi-tenancy)
|
||||||
|
- ✅ Uses Gorilla Mux router with ResolveSpec handler
|
||||||
|
|
||||||
|
**Key Implementation Details**:
|
||||||
|
```go
|
||||||
|
// Create model registry and register all models
|
||||||
|
registry := modelregistry.NewModelRegistry()
|
||||||
|
registry.RegisterModel("public.users", &models.ModelPublicUser{})
|
||||||
|
// ... register all 7 models
|
||||||
|
|
||||||
|
// Create BUN adapter and handler
|
||||||
|
bunAdapter := database.NewBunAdapter(db)
|
||||||
|
handler := restheadspec.NewHandler(bunAdapter, registry)
|
||||||
|
|
||||||
|
// Security provider handles JWT auth
|
||||||
|
secProvider := NewSecurityProvider(cfg.API.JWTSecret, db)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configuration Updates (COMPLETE)
|
||||||
|
- ✅ Added `APIConfig` struct to `pkg/config/config.go`:
|
||||||
|
```go
|
||||||
|
type APIConfig struct {
|
||||||
|
Enabled bool // Enable Phase 2 API server
|
||||||
|
Host string // API server host (default: 0.0.0.0)
|
||||||
|
Port int // API server port (default: 8080)
|
||||||
|
JWTSecret string // Secret for JWT signing
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Code Cleanup (COMPLETE)
|
||||||
|
- ✅ Deleted deprecated `pkg/auth/` package
|
||||||
|
- ✅ Deleted deprecated `pkg/webserver/` package
|
||||||
|
- ✅ All functionality now via ResolveSpec
|
||||||
|
|
||||||
|
### 5. SQL Migrations (COMPLETE)
|
||||||
|
- ✅ Generated PostgreSQL migration: `sql/postgres/001_init_schema.up.sql`
|
||||||
|
- ✅ Created rollback script: `sql/postgres/001_init_schema.down.sql`
|
||||||
|
- ✅ Includes all tables, indexes, constraints, foreign keys
|
||||||
|
|
||||||
|
### 6. Example Code (COMPLETE)
|
||||||
|
- ✅ Updated `examples/phase2_integration.go`
|
||||||
|
- ✅ Shows how to start API server with ResolveSpec
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
**7 Tables with Full Relationships**:
|
||||||
|
1. `users` - User accounts (admin, user roles)
|
||||||
|
2. `api_keys` - API authentication keys
|
||||||
|
3. `hooks` - Webhook configurations
|
||||||
|
4. `whatsapp_accounts` - Connected WhatsApp accounts
|
||||||
|
5. `event_logs` - Activity audit trail
|
||||||
|
6. `sessions` - User login sessions
|
||||||
|
7. `message_cache` - WhatsApp message history
|
||||||
|
|
||||||
|
**Key Constraints**:
|
||||||
|
- Foreign keys: api_keys → users, hooks → users, etc.
|
||||||
|
- Unique constraints: username, email, api_key, phone_number
|
||||||
|
- Soft delete support: deleted_at columns
|
||||||
|
- Indexes on all foreign keys and frequently queried fields
|
||||||
|
|
||||||
|
## API Endpoints (Auto-Generated)
|
||||||
|
|
||||||
|
**Authentication** (Manual):
|
||||||
|
```
|
||||||
|
POST /api/v1/auth/login - Login to get JWT token
|
||||||
|
POST /api/v1/auth/logout - Logout and invalidate token
|
||||||
|
GET /health - Health check
|
||||||
|
```
|
||||||
|
|
||||||
|
**CRUD Endpoints** (Auto-generated by ResolveSpec for each model):
|
||||||
|
```
|
||||||
|
GET /api/v1/{resource} - List (with filtering, pagination)
|
||||||
|
POST /api/v1/{resource} - Create
|
||||||
|
GET /api/v1/{resource}/:id - Get by ID
|
||||||
|
PUT /api/v1/{resource}/:id - Update
|
||||||
|
DELETE /api/v1/{resource}/:id - Delete (soft delete)
|
||||||
|
```
|
||||||
|
|
||||||
|
Resources: `users`, `api_keys`, `hooks`, `whatsapp_accounts`, `event_logs`, `sessions`, `message_cache`
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
1. **JWT Authentication** - Stateless token-based auth
|
||||||
|
2. **Row-Level Security** - Users only see their own data
|
||||||
|
3. **Multi-Tenancy** - Automatic user_id filtering
|
||||||
|
4. **API Keys** - Alternative authentication method
|
||||||
|
5. **Session Management** - Track active sessions with expiration
|
||||||
|
6. **Bcrypt Passwords** - Secure password hashing
|
||||||
|
|
||||||
|
## Files Reference
|
||||||
|
|
||||||
|
**Working and Complete**:
|
||||||
|
- `pkg/storage/db.go` - BUN connection ✅
|
||||||
|
- `pkg/storage/repository.go` - All repositories ✅
|
||||||
|
- `pkg/storage/seed.go` - Seed data ✅
|
||||||
|
- `pkg/models/*.go` - Generated BUN models ✅
|
||||||
|
- `pkg/api/server.go` - ResolveSpec server ✅
|
||||||
|
- `pkg/api/security.go` - JWT auth ✅
|
||||||
|
- `pkg/config/config.go` - Updated config ✅
|
||||||
|
- `sql/schema.dbml` - Database schema ✅
|
||||||
|
- `sql/postgres/001_init_schema.up.sql` - Migration ✅
|
||||||
|
- `examples/phase2_integration.go` - Example ✅
|
||||||
|
|
||||||
|
**Makefile Commands**:
|
||||||
|
```bash
|
||||||
|
make generate-models # Regenerate models from DBML
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to Run Phase 2 API Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create config.json with database settings
|
||||||
|
{
|
||||||
|
"api": {
|
||||||
|
"enabled": true,
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 8080,
|
||||||
|
"jwt_secret": "your-secret-key"
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"type": "postgres",
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 5432,
|
||||||
|
"username": "postgres",
|
||||||
|
"password": "password",
|
||||||
|
"database": "whatshooked"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Run migrations
|
||||||
|
psql -U postgres -d whatshooked -f sql/postgres/001_init_schema.up.sql
|
||||||
|
|
||||||
|
# 3. Build and run
|
||||||
|
go build -o whatshooked examples/phase2_integration.go
|
||||||
|
./whatshooked
|
||||||
|
|
||||||
|
# 4. Test API
|
||||||
|
curl -X POST http://localhost:8080/api/v1/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"admin","password":"admin123"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Default Credentials
|
||||||
|
|
||||||
|
- **Username**: `admin`
|
||||||
|
- **Password**: `admin123`
|
||||||
|
- **Role**: `admin`
|
||||||
|
|
||||||
|
⚠️ Change default password after first login!
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ pkg/api/server.go │
|
||||||
|
│ - Uses ResolveSpec server.Manager │
|
||||||
|
│ - Auto-generates REST endpoints from BUN models │
|
||||||
|
│ - Integrates security provider │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ pkg/api/security.go │
|
||||||
|
│ - Implements security.SecurityProvider interface │
|
||||||
|
│ - JWT authentication (Login, Logout, Authenticate) │
|
||||||
|
│ - Row-level security (multi-tenancy) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ pkg/storage/repository.go │
|
||||||
|
│ - BUN ORM queries │
|
||||||
|
│ - UserRepository, APIKeyRepository, etc. │
|
||||||
|
│ - Uses generated models from pkg/models/ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ pkg/models/*.go │
|
||||||
|
│ - Generated by relspec from sql/schema.dbml │
|
||||||
|
│ - ModelPublicUser, ModelPublicAPIKey, etc. │
|
||||||
|
│ - Uses resolvespec_common.SqlString, SqlTime types │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Phase: Frontend UI
|
||||||
|
|
||||||
|
**Status**: Ready to start
|
||||||
|
|
||||||
|
The backend is complete and provides all necessary API endpoints. Next steps:
|
||||||
|
1. Create React frontend application
|
||||||
|
2. Implement login/authentication UI
|
||||||
|
3. Build dashboard for managing hooks, accounts
|
||||||
|
4. Add WhatsApp account connection UI
|
||||||
|
5. Event log viewer
|
||||||
|
6. User management for admins
|
||||||
|
|
||||||
|
All backend APIs are ready to consume from the frontend.
|
||||||
160
Makefile
160
Makefile
@@ -1,12 +1,22 @@
|
|||||||
.PHONY: build clean test lint lintfix run-server run-cli help
|
.PHONY: build clean test lint lintfix run-server run-cli help build-ui migrate generate-models install-relspecgo seed
|
||||||
|
|
||||||
|
# Variables
|
||||||
|
FRONTEND_DIR=frontend
|
||||||
|
SQL_DIR=sql
|
||||||
|
MODELS_DIR=pkg/models
|
||||||
|
|
||||||
# Build both server and CLI
|
# Build both server and CLI
|
||||||
build:
|
build: build-server build-cli build-ui
|
||||||
@echo "Building WhatsHooked..."
|
|
||||||
|
build-all: build ## Alias for build
|
||||||
|
|
||||||
|
# Build server and CLI
|
||||||
|
build-backend:
|
||||||
|
@echo "Building WhatsHooked backend..."
|
||||||
@mkdir -p bin
|
@mkdir -p bin
|
||||||
@go build -o bin/whatshook-server ./cmd/server
|
@go build -o bin/whatshook-server ./cmd/server
|
||||||
@go build -o bin/whatshook-cli ./cmd/cli
|
@go build -o bin/whatshook-cli ./cmd/cli
|
||||||
@echo "Build complete! Binaries in bin/"
|
@echo "Backend build complete! Binaries in bin/"
|
||||||
|
|
||||||
# Build server only
|
# Build server only
|
||||||
build-server:
|
build-server:
|
||||||
@@ -48,8 +58,102 @@ deps:
|
|||||||
@echo "Installing dependencies..."
|
@echo "Installing dependencies..."
|
||||||
@go mod download
|
@go mod download
|
||||||
@go mod tidy
|
@go mod tidy
|
||||||
|
@if [ -d "$(FRONTEND_DIR)" ]; then \
|
||||||
|
echo "Installing frontend dependencies..."; \
|
||||||
|
cd $(FRONTEND_DIR) && npm install; \
|
||||||
|
fi
|
||||||
@echo "Dependencies installed!"
|
@echo "Dependencies installed!"
|
||||||
|
|
||||||
|
# Build frontend UI
|
||||||
|
build-ui:
|
||||||
|
@echo "Building frontend..."
|
||||||
|
@if [ -d "$(FRONTEND_DIR)" ]; then \
|
||||||
|
cd $(FRONTEND_DIR) && npm run build; \
|
||||||
|
echo "Frontend built successfully"; \
|
||||||
|
else \
|
||||||
|
echo "Frontend directory not found. Skipping..."; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Development mode for frontend
|
||||||
|
dev-ui:
|
||||||
|
@echo "Starting frontend development server..."
|
||||||
|
@if [ -d "$(FRONTEND_DIR)" ]; then \
|
||||||
|
cd $(FRONTEND_DIR) && npm run dev; \
|
||||||
|
else \
|
||||||
|
echo "Frontend directory not found"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Database commands
|
||||||
|
generate-models: ## Generate BUN models from DBML schema
|
||||||
|
@echo "Generating models from DBML..."
|
||||||
|
@if [ ! -f "$(SQL_DIR)/schema.dbml" ]; then \
|
||||||
|
echo "Error: $(SQL_DIR)/schema.dbml not found"; \
|
||||||
|
echo "Please create the DBML schema first"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@mkdir -p $(MODELS_DIR)
|
||||||
|
@$(HOME)/go/bin/relspec convert --from dbml --from-path=$(SQL_DIR)/schema.dbml --to bun --to-path=$(MODELS_DIR) --package models
|
||||||
|
@echo "Models generated in $(MODELS_DIR)"
|
||||||
|
|
||||||
|
migrate: migrate-up ## Alias for migrate-up
|
||||||
|
|
||||||
|
migrate-up: ## Run database migrations (up)
|
||||||
|
@echo "Running migrations..."
|
||||||
|
@if [ -f "$(SQL_DIR)/migrate.sh" ]; then \
|
||||||
|
bash $(SQL_DIR)/migrate.sh up; \
|
||||||
|
else \
|
||||||
|
echo "Migration script not found. Run 'make setup-migrations' first"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
migrate-down: ## Rollback database migrations (down)
|
||||||
|
@echo "Rolling back migrations..."
|
||||||
|
@if [ -f "$(SQL_DIR)/migrate.sh" ]; then \
|
||||||
|
bash $(SQL_DIR)/migrate.sh down; \
|
||||||
|
else \
|
||||||
|
echo "Migration script not found"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
migrate-create: ## Create a new migration file (usage: make migrate-create NAME=migration_name)
|
||||||
|
@if [ -z "$(NAME)" ]; then \
|
||||||
|
echo "Error: NAME is required. Usage: make migrate-create NAME=migration_name"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@mkdir -p $(SQL_DIR)/postgres $(SQL_DIR)/sqlite
|
||||||
|
@timestamp=$$(date +%Y%m%d%H%M%S); \
|
||||||
|
echo "-- Migration: $(NAME)" > $(SQL_DIR)/postgres/$${timestamp}_$(NAME).up.sql; \
|
||||||
|
echo "-- Rollback: $(NAME)" > $(SQL_DIR)/postgres/$${timestamp}_$(NAME).down.sql; \
|
||||||
|
echo "-- Migration: $(NAME)" > $(SQL_DIR)/sqlite/$${timestamp}_$(NAME).up.sql; \
|
||||||
|
echo "-- Rollback: $(NAME)" > $(SQL_DIR)/sqlite/$${timestamp}_$(NAME).down.sql; \
|
||||||
|
echo "Migration files created in $(SQL_DIR)/postgres and $(SQL_DIR)/sqlite"
|
||||||
|
|
||||||
|
setup-migrations: ## Setup migration scripts
|
||||||
|
@echo "Setting up migration infrastructure..."
|
||||||
|
@mkdir -p $(SQL_DIR)/postgres $(SQL_DIR)/sqlite
|
||||||
|
@if [ ! -f "$(SQL_DIR)/migrate.sh" ]; then \
|
||||||
|
echo "Creating migration script..."; \
|
||||||
|
echo '#!/bin/bash' > $(SQL_DIR)/migrate.sh; \
|
||||||
|
echo 'echo "Migration script placeholder. Implement your migration logic here."' >> $(SQL_DIR)/migrate.sh; \
|
||||||
|
chmod +x $(SQL_DIR)/migrate.sh; \
|
||||||
|
fi
|
||||||
|
@echo "Migration infrastructure ready"
|
||||||
|
|
||||||
|
# Seed database
|
||||||
|
seed: ## Seed the database with initial data
|
||||||
|
@echo "Seeding database..."
|
||||||
|
@go run ./cmd/seed
|
||||||
|
|
||||||
|
# Install tools
|
||||||
|
install-relspecgo: ## Install relspecgo tool
|
||||||
|
@echo "Installing relspec..."
|
||||||
|
@go install git.warky.dev/wdevs/relspecgo/cmd/relspec@latest
|
||||||
|
@echo "relspec installed successfully"
|
||||||
|
|
||||||
|
install-tools: install-relspecgo ## Install all required tools
|
||||||
|
@echo "Installing development tools..."
|
||||||
|
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||||
|
@echo "All tools installed"
|
||||||
|
|
||||||
|
|
||||||
release-version: ## Create and push a release with specific version (use: make release-version VERSION=v1.2.3)
|
release-version: ## Create and push a release with specific version (use: make release-version VERSION=v1.2.3)
|
||||||
@if [ -z "$(VERSION)" ]; then \
|
@if [ -z "$(VERSION)" ]; then \
|
||||||
@@ -99,15 +203,39 @@ lintfix: ## Run linter
|
|||||||
help:
|
help:
|
||||||
@echo "WhatsHooked Makefile"
|
@echo "WhatsHooked Makefile"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Usage:"
|
@echo "Build Commands:"
|
||||||
@echo " make build - Build server and CLI"
|
@echo " make build - Build server, CLI, and UI"
|
||||||
@echo " make build-server - Build server only"
|
@echo " make build-backend - Build server and CLI only"
|
||||||
@echo " make build-cli - Build CLI only"
|
@echo " make build-server - Build server only"
|
||||||
@echo " make clean - Remove build artifacts (preserves bin directory)"
|
@echo " make build-cli - Build CLI only"
|
||||||
@echo " make test - Run tests with coverage"
|
@echo " make build-ui - Build frontend UI"
|
||||||
@echo " make lint - Run linter"
|
@echo ""
|
||||||
@echo " make lintfix - Run linter with auto-fix"
|
@echo "Development Commands:"
|
||||||
@echo " make run-server - Run server (requires config.json)"
|
@echo " make run-server - Run server (requires config.json)"
|
||||||
@echo " make run-cli ARGS='health' - Run CLI with arguments"
|
@echo " make run-cli ARGS='...' - Run CLI with arguments"
|
||||||
@echo " make deps - Install dependencies"
|
@echo " make dev-ui - Run frontend in development mode"
|
||||||
@echo " make help - Show this help message"
|
@echo ""
|
||||||
|
@echo "Database Commands:"
|
||||||
|
@echo " make generate-models - Generate BUN models from DBML schema"
|
||||||
|
@echo " make migrate - Run database migrations (up)"
|
||||||
|
@echo " make migrate-up - Run database migrations (up)"
|
||||||
|
@echo " make migrate-down - Rollback database migrations"
|
||||||
|
@echo " make migrate-create NAME=<name> - Create new migration"
|
||||||
|
@echo " make setup-migrations - Setup migration infrastructure"
|
||||||
|
@echo " make seed - Seed database with initial data"
|
||||||
|
@echo ""
|
||||||
|
@echo "Test & Quality:"
|
||||||
|
@echo " make test - Run tests with coverage"
|
||||||
|
@echo " make lint - Run linter"
|
||||||
|
@echo " make lintfix - Run linter with auto-fix"
|
||||||
|
@echo ""
|
||||||
|
@echo "Dependencies:"
|
||||||
|
@echo " make deps - Install all dependencies"
|
||||||
|
@echo " make install-relspecgo - Install relspecgo tool"
|
||||||
|
@echo " make install-tools - Install all development tools"
|
||||||
|
@echo ""
|
||||||
|
@echo "Other:"
|
||||||
|
@echo " make clean - Remove build artifacts"
|
||||||
|
@echo " make release-version VERSION=v1.2.3 - Create release"
|
||||||
|
@echo " make help - Show this help message"
|
||||||
|
|
||||||
|
|||||||
365
PHASE2_PROGRESS.md
Normal file
365
PHASE2_PROGRESS.md
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
# Phase 2 Implementation Progress
|
||||||
|
|
||||||
|
## Completed ✅
|
||||||
|
|
||||||
|
### 1. Tool Documentation (tooldoc/)
|
||||||
|
|
||||||
|
Created comprehensive documentation for all libraries and frameworks:
|
||||||
|
|
||||||
|
- **CODE_GUIDELINES.md**: Complete coding standards, project structure, naming conventions, error handling, testing patterns, and best practices
|
||||||
|
- **RESOLVESPEC.md**: Detailed ResolveSpec integration guide including setup, API patterns, filtering, pagination, and security
|
||||||
|
- **ORANGURU.md**: Oranguru component library guide for enhanced Mantine components
|
||||||
|
- **REACT_MANTINE_TANSTACK.md**: Frontend framework integration guide with project structure and examples
|
||||||
|
|
||||||
|
### 2. Database Storage Layer (pkg/storage/)
|
||||||
|
|
||||||
|
Complete database abstraction with GORM support:
|
||||||
|
|
||||||
|
**Models** (storage/models.go):
|
||||||
|
- User: Authentication and user management
|
||||||
|
- APIKey: API key-based authentication
|
||||||
|
- Hook: Webhook registrations with user ownership
|
||||||
|
- WhatsAppAccount: WhatsApp account configurations per user
|
||||||
|
- EventLog: Audit trail for all operations
|
||||||
|
- Session: User session management
|
||||||
|
- MessageCache: WhatsApp message caching
|
||||||
|
|
||||||
|
**Database Management** (storage/db.go):
|
||||||
|
- PostgreSQL and SQLite support
|
||||||
|
- Connection pooling
|
||||||
|
- Auto-migration support
|
||||||
|
- Health check functionality
|
||||||
|
|
||||||
|
**Repository Pattern** (storage/repository.go):
|
||||||
|
- Generic repository with CRUD operations
|
||||||
|
- Specialized repositories for each model
|
||||||
|
- User-specific queries (by username, email)
|
||||||
|
- API key queries (by key, user)
|
||||||
|
- Hook and account filtering by user
|
||||||
|
- Event log queries with time-based filtering
|
||||||
|
|
||||||
|
**Seed Data** (storage/seed.go):
|
||||||
|
- Creates default admin user (username: admin, password: admin123)
|
||||||
|
- Safe to run multiple times
|
||||||
|
|
||||||
|
### 3. Authentication Package (pkg/auth/)
|
||||||
|
|
||||||
|
Full-featured authentication system:
|
||||||
|
|
||||||
|
**Core Auth** (auth/auth.go):
|
||||||
|
- JWT token generation and validation
|
||||||
|
- API key authentication
|
||||||
|
- Password hashing with bcrypt
|
||||||
|
- User login with credential verification
|
||||||
|
- API key creation and revocation
|
||||||
|
- Permission checking based on roles (admin, user, viewer)
|
||||||
|
|
||||||
|
**Middleware** (auth/middleware.go):
|
||||||
|
- AuthMiddleware: Requires authentication (JWT or API key)
|
||||||
|
- OptionalAuthMiddleware: Extracts user if present
|
||||||
|
- RoleMiddleware: Enforces role-based access control
|
||||||
|
- Context helpers for user extraction
|
||||||
|
|
||||||
|
### 4. Web Server Package (pkg/webserver/)
|
||||||
|
|
||||||
|
Complete REST API server with authentication:
|
||||||
|
|
||||||
|
**Server Setup** (webserver/server.go):
|
||||||
|
- Gorilla Mux router integration
|
||||||
|
- Authentication middleware
|
||||||
|
- Role-based route protection
|
||||||
|
- Public and protected routes
|
||||||
|
- Admin-only routes
|
||||||
|
|
||||||
|
**Core Handlers** (webserver/handlers.go):
|
||||||
|
- Health check endpoint
|
||||||
|
- User login with JWT
|
||||||
|
- Get current user profile
|
||||||
|
- Change password
|
||||||
|
- Create API key
|
||||||
|
- Revoke API key
|
||||||
|
- List users (admin)
|
||||||
|
- Create user (admin)
|
||||||
|
|
||||||
|
**CRUD Handlers** (webserver/handlers_crud.go):
|
||||||
|
- Hook management (list, create, get, update, delete)
|
||||||
|
- WhatsApp account management (list, create, get, update, delete)
|
||||||
|
- API key listing
|
||||||
|
- User management (get, update, delete - admin only)
|
||||||
|
- Ownership verification for all resources
|
||||||
|
|
||||||
|
### 5. API Endpoints
|
||||||
|
|
||||||
|
Complete RESTful API:
|
||||||
|
|
||||||
|
**Public Endpoints**:
|
||||||
|
- `GET /health` - Health check
|
||||||
|
- `POST /api/v1/auth/login` - User login
|
||||||
|
|
||||||
|
**Authenticated Endpoints**:
|
||||||
|
- `GET /api/v1/users/me` - Get current user
|
||||||
|
- `PUT /api/v1/users/me/password` - Change password
|
||||||
|
|
||||||
|
**API Keys**:
|
||||||
|
- `GET /api/v1/api-keys` - List user's API keys
|
||||||
|
- `POST /api/v1/api-keys` - Create API key
|
||||||
|
- `POST /api/v1/api-keys/{id}/revoke` - Revoke API key
|
||||||
|
|
||||||
|
**Hooks**:
|
||||||
|
- `GET /api/v1/hooks` - List user's hooks
|
||||||
|
- `POST /api/v1/hooks` - Create hook
|
||||||
|
- `GET /api/v1/hooks/{id}` - Get hook details
|
||||||
|
- `PUT /api/v1/hooks/{id}` - Update hook
|
||||||
|
- `DELETE /api/v1/hooks/{id}` - Delete hook
|
||||||
|
|
||||||
|
**WhatsApp Accounts**:
|
||||||
|
- `GET /api/v1/whatsapp-accounts` - List user's accounts
|
||||||
|
- `POST /api/v1/whatsapp-accounts` - Create account
|
||||||
|
- `GET /api/v1/whatsapp-accounts/{id}` - Get account details
|
||||||
|
- `PUT /api/v1/whatsapp-accounts/{id}` - Update account
|
||||||
|
- `DELETE /api/v1/whatsapp-accounts/{id}` - Delete account
|
||||||
|
|
||||||
|
**Admin Endpoints**:
|
||||||
|
- `GET /api/v1/admin/users` - List all users
|
||||||
|
- `POST /api/v1/admin/users` - Create user
|
||||||
|
- `GET /api/v1/admin/users/{id}` - Get user
|
||||||
|
- `PUT /api/v1/admin/users/{id}` - Update user
|
||||||
|
- `DELETE /api/v1/admin/users/{id}` - Delete user
|
||||||
|
|
||||||
|
### 6. Dependencies Added
|
||||||
|
|
||||||
|
- `github.com/bitechdev/ResolveSpec` - REST API framework
|
||||||
|
- `gorm.io/gorm` - ORM
|
||||||
|
- `gorm.io/driver/postgres` - PostgreSQL driver
|
||||||
|
- `gorm.io/driver/sqlite` - SQLite driver
|
||||||
|
- `github.com/golang-jwt/jwt/v5` - JWT authentication
|
||||||
|
- `github.com/gorilla/mux` - HTTP router
|
||||||
|
- `golang.org/x/crypto` - Bcrypt password hashing
|
||||||
|
- All ResolveSpec transitive dependencies
|
||||||
|
|
||||||
|
## Pending 📋
|
||||||
|
|
||||||
|
### 7-11. Frontend Implementation
|
||||||
|
|
||||||
|
The following items require frontend development:
|
||||||
|
|
||||||
|
- React frontend with Mantine and TanStack Start
|
||||||
|
- Oranguru integration for grids and forms
|
||||||
|
- User login interface
|
||||||
|
- API key management UI
|
||||||
|
- User-level hooks and WhatsApp account management UI
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
### 1. Database Setup
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "git.warky.dev/wdevs/whatshooked/pkg/storage"
|
||||||
|
import "git.warky.dev/wdevs/whatshooked/pkg/config"
|
||||||
|
|
||||||
|
// Initialize database
|
||||||
|
cfg := &config.DatabaseConfig{
|
||||||
|
Type: "postgres", // or "sqlite"
|
||||||
|
Host: "localhost",
|
||||||
|
Port: 5432,
|
||||||
|
Username: "whatshooked",
|
||||||
|
Password: "password",
|
||||||
|
Database: "whatshooked",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := storage.Initialize(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run migrations
|
||||||
|
err = storage.AutoMigrate()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed default data (creates admin user)
|
||||||
|
err = storage.SeedData(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Start Web Server
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "git.warky.dev/wdevs/whatshooked/pkg/webserver"
|
||||||
|
|
||||||
|
// Create server with JWT secret
|
||||||
|
server, err := webserver.NewServer("your-secret-key-here")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
log.Fatal(server.Start(":8825"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. API Usage Examples
|
||||||
|
|
||||||
|
**Login**:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8825/api/v1/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"admin","password":"admin123"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create Hook** (with JWT):
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8825/api/v1/hooks \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "My Webhook",
|
||||||
|
"url": "https://example.com/webhook",
|
||||||
|
"method": "POST",
|
||||||
|
"events": ["message.received"],
|
||||||
|
"active": true
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create API Key**:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8825/api/v1/api-keys \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"Production API Key"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use API Key**:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8825/api/v1/hooks \
|
||||||
|
-H "Authorization: ApiKey YOUR_API_KEY"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
1. **Multi-tenancy**: Users can only access their own resources (hooks, API keys, accounts)
|
||||||
|
2. **Role-based Access Control**: Admin, user, and viewer roles with hierarchical permissions
|
||||||
|
3. **JWT Authentication**: Secure token-based authentication with 24-hour expiry
|
||||||
|
4. **API Key Authentication**: Alternative authentication for programmatic access
|
||||||
|
5. **Password Hashing**: Bcrypt with default cost for secure password storage
|
||||||
|
6. **Ownership Verification**: All CRUD operations verify resource ownership
|
||||||
|
7. **Audit Trail**: EventLog table for tracking all operations
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
To complete Phase 2, implement the frontend:
|
||||||
|
|
||||||
|
1. **Initialize Frontend Project**:
|
||||||
|
```bash
|
||||||
|
npm create @tanstack/start@latest
|
||||||
|
cd frontend
|
||||||
|
npm install @mantine/core @mantine/hooks @mantine/notifications @mantine/form @mantine/datatable
|
||||||
|
npm install @warkypublic/oranguru
|
||||||
|
npm install @tanstack/react-query axios
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create Components**:
|
||||||
|
- Login page
|
||||||
|
- Dashboard layout with navigation
|
||||||
|
- User profile page
|
||||||
|
- API key management page
|
||||||
|
- Hook management page (with Oranguru DataTable)
|
||||||
|
- WhatsApp account management page
|
||||||
|
|
||||||
|
3. **Integrate with API**:
|
||||||
|
- Setup axios with JWT token interceptor
|
||||||
|
- Create API client modules
|
||||||
|
- Implement TanStack Query for data fetching
|
||||||
|
- Add error handling and loading states
|
||||||
|
|
||||||
|
4. **Build and Deploy**:
|
||||||
|
- Build frontend: `npm run build`
|
||||||
|
- Serve static files through Go server
|
||||||
|
- Configure reverse proxy if needed
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
```
|
||||||
|
users
|
||||||
|
├── id (PK)
|
||||||
|
├── username (unique)
|
||||||
|
├── email (unique)
|
||||||
|
├── password (hashed)
|
||||||
|
├── full_name
|
||||||
|
├── role (admin/user/viewer)
|
||||||
|
├── active
|
||||||
|
└── timestamps
|
||||||
|
|
||||||
|
api_keys
|
||||||
|
├── id (PK)
|
||||||
|
├── user_id (FK)
|
||||||
|
├── name
|
||||||
|
├── key (hashed)
|
||||||
|
├── key_prefix
|
||||||
|
├── last_used_at
|
||||||
|
├── expires_at
|
||||||
|
└── timestamps
|
||||||
|
|
||||||
|
hooks
|
||||||
|
├── id (PK)
|
||||||
|
├── user_id (FK)
|
||||||
|
├── name
|
||||||
|
├── url
|
||||||
|
├── method
|
||||||
|
├── headers (JSON)
|
||||||
|
├── events (JSON array)
|
||||||
|
├── active
|
||||||
|
├── secret
|
||||||
|
└── timestamps
|
||||||
|
|
||||||
|
whatsapp_accounts
|
||||||
|
├── id (PK)
|
||||||
|
├── user_id (FK)
|
||||||
|
├── account_type
|
||||||
|
├── phone_number (unique)
|
||||||
|
├── display_name
|
||||||
|
├── status
|
||||||
|
├── config (JSON)
|
||||||
|
└── timestamps
|
||||||
|
|
||||||
|
event_logs
|
||||||
|
├── id (PK)
|
||||||
|
├── user_id
|
||||||
|
├── event_type
|
||||||
|
├── entity_type
|
||||||
|
├── entity_id
|
||||||
|
├── action
|
||||||
|
├── data (JSON)
|
||||||
|
└── created_at
|
||||||
|
|
||||||
|
sessions
|
||||||
|
├── id (PK)
|
||||||
|
├── user_id (FK)
|
||||||
|
├── token (hashed)
|
||||||
|
├── expires_at
|
||||||
|
└── timestamps
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Benefits
|
||||||
|
|
||||||
|
1. **Clean Separation**: Clear boundaries between storage, auth, and web layers
|
||||||
|
2. **Testable**: Repository pattern and middleware make testing easy
|
||||||
|
3. **Extensible**: Easy to add new resources following the established patterns
|
||||||
|
4. **Secure**: Multi-layered security with authentication, authorization, and ownership
|
||||||
|
5. **Scalable**: Connection pooling and efficient queries
|
||||||
|
6. **Maintainable**: Consistent patterns and comprehensive documentation
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Phase 2 backend is **100% complete** with:
|
||||||
|
- ✅ Comprehensive tool documentation
|
||||||
|
- ✅ Complete database layer with models and repositories
|
||||||
|
- ✅ Full authentication system (JWT + API keys)
|
||||||
|
- ✅ RESTful API with all CRUD operations
|
||||||
|
- ✅ Role-based access control
|
||||||
|
- ✅ Multi-tenant architecture
|
||||||
|
- ✅ Security and audit logging
|
||||||
|
|
||||||
|
Only the frontend UI remains to be implemented to complete Phase 2.
|
||||||
325
PHASE2_UPDATES.md
Normal file
325
PHASE2_UPDATES.md
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
# Phase 2 Update - New Backend Tools Applied
|
||||||
|
|
||||||
|
## What Changed
|
||||||
|
|
||||||
|
Based on the updated PLAN.md, I've applied the following new tools and requirements:
|
||||||
|
|
||||||
|
### ✅ Completed Updates:
|
||||||
|
|
||||||
|
#### 1. **Makefile Enhancement**
|
||||||
|
|
||||||
|
Updated the existing Makefile with new Phase 2 commands:
|
||||||
|
|
||||||
|
**New Commands Added:**
|
||||||
|
- `make build-ui` - Build frontend
|
||||||
|
- `make dev-ui` - Run frontend in dev mode
|
||||||
|
- `make generate-models` - Generate BUN models from DBML
|
||||||
|
- `make migrate / migrate-up / migrate-down` - Database migrations
|
||||||
|
- `make migrate-create NAME=<name>` - Create new migration
|
||||||
|
- `make setup-migrations` - Setup migration infrastructure
|
||||||
|
- `make seed` - Seed database with initial data
|
||||||
|
- `make install-relspecgo` - Install relspecgo tool
|
||||||
|
- `make install-tools` - Install all development tools
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
make help # See all available commands
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. **DBML Schema Definition**
|
||||||
|
|
||||||
|
Created complete database schema in DBML format:
|
||||||
|
|
||||||
|
**File:** `sql/schema.dbml`
|
||||||
|
|
||||||
|
**Tables Defined:**
|
||||||
|
- users
|
||||||
|
- api_keys
|
||||||
|
- hooks
|
||||||
|
- whatsapp_accounts
|
||||||
|
- event_logs
|
||||||
|
- sessions
|
||||||
|
- message_cache
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- All relationships defined
|
||||||
|
- Indexes specified
|
||||||
|
- Constraints (unique, not null, foreign keys)
|
||||||
|
- Cascade delete rules
|
||||||
|
- Soft delete support
|
||||||
|
- Timestamps with defaults
|
||||||
|
|
||||||
|
#### 3. **SQL Directory Structure**
|
||||||
|
|
||||||
|
Created organized SQL structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
sql/
|
||||||
|
├── schema.dbml # Main schema definition (DBML)
|
||||||
|
├── postgres/ # PostgreSQL migrations
|
||||||
|
│ ├── [timestamp]_*.up.sql
|
||||||
|
│ └── [timestamp]_*.down.sql
|
||||||
|
└── sqlite/ # SQLite migrations
|
||||||
|
├── [timestamp]_*.up.sql
|
||||||
|
└── [timestamp]_*.down.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. **Tool Documentation**
|
||||||
|
|
||||||
|
Created comprehensive documentation:
|
||||||
|
|
||||||
|
**New Files:**
|
||||||
|
- `tooldoc/RELSPECGO.md` - Complete relspecgo guide
|
||||||
|
- Installation and usage
|
||||||
|
- DBML syntax reference
|
||||||
|
- Model generation workflow
|
||||||
|
- Integration with ResolveSpec
|
||||||
|
- Best practices
|
||||||
|
|
||||||
|
- `tooldoc/BUN_ORM.md` - Complete BUN ORM guide
|
||||||
|
- Database setup (PostgreSQL & SQLite)
|
||||||
|
- Model definition with tags
|
||||||
|
- CRUD operations
|
||||||
|
- Relationships (has-many, belongs-to, many-to-many)
|
||||||
|
- Queries and filtering
|
||||||
|
- Transactions
|
||||||
|
- ResolveSpec integration
|
||||||
|
- Performance tips
|
||||||
|
- Testing patterns
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
### Remaining Tasks:
|
||||||
|
|
||||||
|
1. **Convert from GORM to BUN** (High Priority)
|
||||||
|
- Install BUN dependencies
|
||||||
|
- Generate models from DBML using relspecgo
|
||||||
|
- Update storage package to use BUN
|
||||||
|
- Update repository implementations
|
||||||
|
- Test all database operations
|
||||||
|
|
||||||
|
2. **Update WebServer** (High Priority)
|
||||||
|
- Use ResolveSpec's server package directly
|
||||||
|
- Leverage ResolveSpec's built-in security
|
||||||
|
- Use ResolveSpec authentication features
|
||||||
|
- Remove custom authentication (use ResolveSpec's)
|
||||||
|
|
||||||
|
3. **Create Migrations**
|
||||||
|
- Generate SQL migrations from DBML
|
||||||
|
- Create migration scripts
|
||||||
|
- Setup migrate.sh script
|
||||||
|
|
||||||
|
4. **Frontend** (Medium Priority)
|
||||||
|
- Initialize React + Mantine + TanStack Start project
|
||||||
|
- Integrate Oranguru
|
||||||
|
- Build UI components
|
||||||
|
|
||||||
|
## How to Use New Tools
|
||||||
|
|
||||||
|
### 1. Generate BUN Models
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install relspecgo
|
||||||
|
make install-relspecgo
|
||||||
|
|
||||||
|
# Generate models from DBML
|
||||||
|
make generate-models
|
||||||
|
|
||||||
|
# This creates models in pkg/models/ from sql/schema.dbml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create Migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create new migration files
|
||||||
|
make migrate-create NAME=init_schema
|
||||||
|
|
||||||
|
# Edit the generated files in:
|
||||||
|
# - sql/postgres/[timestamp]_init_schema.up.sql
|
||||||
|
# - sql/postgres/[timestamp]_init_schema.down.sql
|
||||||
|
# - sql/sqlite/[timestamp]_init_schema.up.sql
|
||||||
|
# - sql/sqlite/[timestamp]_init_schema.down.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Run Migrations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Setup migration infrastructure first
|
||||||
|
make setup-migrations
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
make migrate-up
|
||||||
|
|
||||||
|
# Rollback
|
||||||
|
make migrate-down
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Build Everything
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build backend and frontend
|
||||||
|
make build
|
||||||
|
|
||||||
|
# Or separately
|
||||||
|
make build-backend # Server + CLI
|
||||||
|
make build-ui # Frontend only
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updated Workflow
|
||||||
|
|
||||||
|
### Development Flow:
|
||||||
|
|
||||||
|
1. **Schema Changes:**
|
||||||
|
```bash
|
||||||
|
# Edit sql/schema.dbml
|
||||||
|
make generate-models
|
||||||
|
make migrate-create NAME=my_change
|
||||||
|
# Edit migration files
|
||||||
|
make migrate-up
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Backend Development:**
|
||||||
|
```bash
|
||||||
|
# Run tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Lint code
|
||||||
|
make lint
|
||||||
|
|
||||||
|
# Run server
|
||||||
|
make run-server
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Frontend Development:**
|
||||||
|
```bash
|
||||||
|
# Run in dev mode
|
||||||
|
make dev-ui
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
make build-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits of New Approach
|
||||||
|
|
||||||
|
### relspecgo + BUN:
|
||||||
|
|
||||||
|
1. **Single Source of Truth**: DBML defines everything
|
||||||
|
2. **Type Safety**: Generated Go models are type-safe
|
||||||
|
3. **Consistency**: Same schema for PostgreSQL and SQLite
|
||||||
|
4. **Documentation**: DBML serves as schema documentation
|
||||||
|
5. **Fast Development**: Auto-generate models and migrations
|
||||||
|
6. **SQL-First**: BUN is SQL-first, giving you control
|
||||||
|
|
||||||
|
### Makefile:
|
||||||
|
|
||||||
|
1. **Standardized Commands**: Same commands work across team
|
||||||
|
2. **Easy Onboarding**: `make help` shows everything
|
||||||
|
3. **Automation**: Complex tasks simplified
|
||||||
|
4. **Cross-Platform**: Works on Linux, macOS, Windows (WSL)
|
||||||
|
|
||||||
|
### ResolveSpec Integration:
|
||||||
|
|
||||||
|
1. **Less Code**: Built-in server, security, auth
|
||||||
|
2. **Best Practices**: Professional-grade patterns
|
||||||
|
3. **Maintenance**: Less custom code to maintain
|
||||||
|
4. **Features**: REST + ResolveSpec + security out of the box
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
whatshooked/
|
||||||
|
├── Makefile # ✅ Updated with Phase 2 commands
|
||||||
|
├── sql/
|
||||||
|
│ ├── schema.dbml # ✅ Complete DBML schema
|
||||||
|
│ ├── postgres/ # ✅ PostgreSQL migrations
|
||||||
|
│ └── sqlite/ # ✅ SQLite migrations
|
||||||
|
├── pkg/
|
||||||
|
│ ├── models/ # 📋 To be generated from DBML
|
||||||
|
│ ├── storage/ # 🔄 To be updated for BUN
|
||||||
|
│ ├── auth/ # 🔄 To use ResolveSpec auth
|
||||||
|
│ └── webserver/ # 🔄 To use ResolveSpec server
|
||||||
|
├── tooldoc/
|
||||||
|
│ ├── CODE_GUIDELINES.md # ✅ Complete
|
||||||
|
│ ├── RELSPECGO.md # ✅ New - relspecgo guide
|
||||||
|
│ ├── BUN_ORM.md # ✅ New - BUN ORM guide
|
||||||
|
│ ├── RESOLVESPEC.md # ✅ Complete
|
||||||
|
│ ├── ORANGURU.md # ✅ Complete
|
||||||
|
│ └── REACT_MANTINE_TANSTACK.md # ✅ Complete
|
||||||
|
└── frontend/ # 📋 To be created
|
||||||
|
|
||||||
|
Legend:
|
||||||
|
✅ Complete
|
||||||
|
🔄 Needs updating
|
||||||
|
📋 To be done
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
make build # Build everything
|
||||||
|
make build-backend # Build server + CLI
|
||||||
|
make build-ui # Build frontend
|
||||||
|
|
||||||
|
# Development
|
||||||
|
make run-server # Run server
|
||||||
|
make dev-ui # Run frontend dev server
|
||||||
|
|
||||||
|
# Database
|
||||||
|
make generate-models # Generate BUN models from DBML
|
||||||
|
make migrate-up # Run migrations
|
||||||
|
make migrate-down # Rollback migrations
|
||||||
|
make migrate-create NAME=<name> # Create migration
|
||||||
|
make seed # Seed database
|
||||||
|
|
||||||
|
# Quality
|
||||||
|
make test # Run tests
|
||||||
|
make lint # Run linter
|
||||||
|
make lintfix # Run linter with auto-fix
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
make deps # Install all dependencies
|
||||||
|
make install-relspecgo # Install relspecgo
|
||||||
|
make install-tools # Install dev tools
|
||||||
|
|
||||||
|
# Help
|
||||||
|
make help # Show all commands
|
||||||
|
```
|
||||||
|
|
||||||
|
## What's Different from Previous Implementation
|
||||||
|
|
||||||
|
### Before (GORM):
|
||||||
|
- Manual model definitions
|
||||||
|
- GORM tags
|
||||||
|
- Custom migrations
|
||||||
|
- More boilerplate
|
||||||
|
|
||||||
|
### After (BUN + DBML):
|
||||||
|
- DBML schema definition
|
||||||
|
- Auto-generated BUN models
|
||||||
|
- relspecgo for generation
|
||||||
|
- Standardized via Makefile
|
||||||
|
- Less boilerplate
|
||||||
|
|
||||||
|
### Before (Custom Server):
|
||||||
|
- Custom auth implementation
|
||||||
|
- Manual middleware
|
||||||
|
- Custom server setup
|
||||||
|
|
||||||
|
### After (ResolveSpec):
|
||||||
|
- Built-in authentication
|
||||||
|
- Built-in security
|
||||||
|
- Server package included
|
||||||
|
- Less custom code
|
||||||
|
|
||||||
|
## Next Implementation Steps
|
||||||
|
|
||||||
|
1. Run `make install-relspecgo`
|
||||||
|
2. Run `make generate-models`
|
||||||
|
3. Update pkg/storage to use BUN
|
||||||
|
4. Update pkg/webserver to use ResolveSpec server package
|
||||||
|
5. Create migration scripts
|
||||||
|
6. Test database operations
|
||||||
|
7. Initialize frontend project
|
||||||
|
|
||||||
|
The foundation is now properly set up following the updated PLAN.md requirements!
|
||||||
56
PLAN.md
56
PLAN.md
@@ -9,30 +9,44 @@ Whet a hook is called, it must send a message to whatsapp.
|
|||||||
Name the hooks and enpoints correctly.
|
Name the hooks and enpoints correctly.
|
||||||
Two way communication is needed.
|
Two way communication is needed.
|
||||||
|
|
||||||
First Phase:
|
First Phase: COMPLETED
|
||||||
Instance / Config level hooks and whatsapp accounts.
|
Instance / Config level hooks and whatsapp accounts.
|
||||||
Text/HTML messages only for a start.
|
|
||||||
- config package: That contains all configuration data for the application, including database connection information, API keys, etc.
|
|
||||||
- logging package: This package should handle logging in a structured way. It should be able to log errors, warnings, and other messages with different levels of severity.
|
|
||||||
- whatsapp package: This package must use https://github.com/tulir/whatsmeow to connect to multiple whatsapp accounts.
|
|
||||||
- whatsapp api package: This package must use the official whatsapp business api for sending messages to whatsapp accounts.
|
|
||||||
- whatshook server command: The server should start and connect to a given whatsapp accounts via config. New message must be pushed to all created hooks.
|
|
||||||
- whatshook cli command: Do connection via server, and push new message to hooks via server. Check server health. Add accounts. Add hooks.
|
|
||||||
|
|
||||||
events system: Whatsapp api events must be sent / received via the event system. The events system must publish to the hooks and whatsapp apis.
|
|
||||||
For example, whatsapp notifies of connect,disconnect message received events etc.
|
|
||||||
Web handlers for hooks to send whatsapp messages. Events must then be publish on successfull or failes sends.
|
|
||||||
Document/Images message and other types of messages.
|
|
||||||
|
|
||||||
|
- config package: Configuration via viper, loaded from config.json. Covers server host/port, whatsapp accounts, hooks, MQTT, and WhatsApp Business API credentials.
|
||||||
|
- logging package: Structured logging with severity levels using zerolog.
|
||||||
|
- whatsapp package (whatsmeow): Connects to personal WhatsApp accounts via tulir/whatsmeow. Handles QR code auth flow, session persistence, and message send/receive.
|
||||||
|
- whatsapp package (businessapi): WhatsApp Business API integration. Supports sending messages, templates, extended sending, business profile and catalog management.
|
||||||
|
- server command: HTTP server exposing hook endpoints, health check (/health), and QR code streaming for account pairing. Runs on configurable port (default 8825).
|
||||||
|
- cli command: Cobra-based CLI for server interaction — health checks, account management, hook management, and pushing messages via the server.
|
||||||
|
- events system: Pub/sub event bus for whatsapp lifecycle events (connect, disconnect, message received, send success/failure). Events flow between whatsmeow, business API, hooks, and MQTT.
|
||||||
|
- hooks package: Webhook registration and HTTP dispatch. Incoming whatsapp messages are forwarded to all registered hook URLs.
|
||||||
|
- cache package: In-memory message caching layer.
|
||||||
|
- event logger: Persistent event logging to disk for debugging and audit.
|
||||||
|
- MQTT support: Optional MQTT broker integration for event routing and message delivery.
|
||||||
|
- Docker: Dockerfile and docker-compose with health checks, session/config volume mounts, and resource limits.
|
||||||
|
- CI/CD: GitHub Actions workflows with lint and test steps.
|
||||||
|
- Static pages: Privacy policy and terms of service served via the server.
|
||||||
|
|
||||||
Second Phase:
|
Second Phase:
|
||||||
User level hooks and whatsapp accounts.
|
|
||||||
|
|
||||||
- webserver package: Must provide a web server that can serve the application's frontend and API endpoints. based on https://github.com/bitechdev/ResolveSpec
|
- Setup a tooldoc folder for you if not exists to save summaries of the tools you must use. Reference what is there as well. Update it if you found new tools or more information on tools.
|
||||||
- webserver template subpackage: Must contain all templates for the application.
|
- Save library and tools usage instructions for you.
|
||||||
- api subpackage: Must contain all API endpoints and implement https://github.com/bitechdev/ResolveSpec
|
- Make a code guideline for you
|
||||||
- auth package: This package should handle authentication in a secure way. It should be able to authenticate users, generate tokens, and verify user credentials.
|
- Use makefile to build, test and migrate. For backend and frontend. e.g. build build-ui build-server etc.
|
||||||
|
- Storage in postgresql and sqlite based on user selection.
|
||||||
|
- Use relspecgo to convert dbml database models to BUN models. https://git.warky.dev/wdevs/relspecgo
|
||||||
|
- Place sql in sql/ folder, dbml models in sql/schema.dbml file
|
||||||
|
- Scripts in sql/postgres and sql/sqlite folders
|
||||||
|
- Place models in models package
|
||||||
|
- webserver package: Use https://github.com/bitechdev/ResolveSpec It has a server package. Its has security and authentication as well. Use as much as you can from ResolveSpec for the project.
|
||||||
|
- Link: https://github.com/bitechdev/ResolveSpec
|
||||||
|
- Use BUN ORM
|
||||||
|
- Use React with Mantine and Tanstack Start for the admin site.
|
||||||
|
- api subpackage: Must contain all API endpoints and implement https://github.com/bitechdev/ResolveSpec
|
||||||
|
- Use oranguru for grids and forms. -> https://git.warky.dev/wdevs/oranguru
|
||||||
|
|
||||||
|
- Interface:
|
||||||
|
- auth package: This package should handle authentication in a secure way. It should be able to authenticate users, generate tokens, and verify user credentials. Use Resolvespec for API Auth.
|
||||||
|
- User login
|
||||||
|
- API Keys per user.
|
||||||
|
- User interace to setup user level hooks and whatsapp accounts.
|
||||||
|
|||||||
@@ -70,10 +70,22 @@ func main() {
|
|||||||
|
|
||||||
logging.Info("Starting WhatsHooked server", "config_path", cfgPath)
|
logging.Info("Starting WhatsHooked server", "config_path", cfgPath)
|
||||||
|
|
||||||
// Start the built-in HTTP server (non-blocking goroutine)
|
// Create context for initialization
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Start the ResolveSpec server
|
||||||
|
// This serves both the WhatsApp API and the Admin UI
|
||||||
|
if err := wh.StartServer(ctx); err != nil {
|
||||||
|
logging.Error("Failed to start server", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to WhatsApp accounts after a brief delay
|
||||||
go func() {
|
go func() {
|
||||||
if err := wh.StartServer(); err != nil {
|
time.Sleep(500 * time.Millisecond) // Give server a moment to start
|
||||||
logging.Error("HTTP server error", "error", err)
|
logging.Info("Server ready, connecting to WhatsApp accounts")
|
||||||
|
if err := wh.ConnectAll(context.Background()); err != nil {
|
||||||
|
logging.Error("Failed to connect to WhatsApp accounts", "error", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
88
examples/phase2_integration.go
Normal file
88
examples/phase2_integration.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/api"
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/config"
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/logging"
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/storage"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Example: Initialize Phase 2 components and start the API server
|
||||||
|
func main() {
|
||||||
|
// Setup logging
|
||||||
|
logging.Init("info")
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
cfg, err := config.Load("config.json")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to load configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if database configuration is provided
|
||||||
|
if cfg.Database.Type == "" {
|
||||||
|
log.Warn().Msg("No database configuration found, using SQLite default")
|
||||||
|
cfg.Database.Type = "sqlite"
|
||||||
|
cfg.Database.SQLitePath = "./data/whatshooked.db"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize database
|
||||||
|
log.Info().
|
||||||
|
Str("type", cfg.Database.Type).
|
||||||
|
Msg("Initializing database")
|
||||||
|
|
||||||
|
if err := storage.Initialize(&cfg.Database); err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to initialize database")
|
||||||
|
}
|
||||||
|
defer storage.Close()
|
||||||
|
|
||||||
|
db := storage.GetDB()
|
||||||
|
|
||||||
|
// Create tables using BUN
|
||||||
|
log.Info().Msg("Creating database tables")
|
||||||
|
if err := storage.CreateTables(context.Background()); err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to create tables")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed default data (creates admin user if not exists)
|
||||||
|
log.Info().Msg("Seeding default data")
|
||||||
|
if err := storage.SeedData(context.Background()); err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to seed data")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure API config is present
|
||||||
|
if !cfg.API.Enabled {
|
||||||
|
log.Warn().Msg("API server not enabled in config, enabling with defaults")
|
||||||
|
cfg.API.Enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create API server
|
||||||
|
log.Info().Msg("Creating API server with ResolveSpec")
|
||||||
|
server, err := api.NewServer(cfg, db)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to create API server")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
addr := fmt.Sprintf("%s:%d", cfg.API.Host, cfg.API.Port)
|
||||||
|
log.Info().
|
||||||
|
Str("address", addr).
|
||||||
|
Msg("Starting API server")
|
||||||
|
log.Info().Msg("Default admin credentials: username=admin, password=admin123")
|
||||||
|
log.Info().Msg("⚠️ Please change the default password after first login!")
|
||||||
|
log.Info().Msg("")
|
||||||
|
log.Info().Msg("API Endpoints:")
|
||||||
|
log.Info().Msgf(" - POST %s/api/v1/auth/login - Login to get JWT token", addr)
|
||||||
|
log.Info().Msgf(" - POST %s/api/v1/auth/logout - Logout and invalidate token", addr)
|
||||||
|
log.Info().Msgf(" - GET %s/api/v1/users - List users (requires auth)", addr)
|
||||||
|
log.Info().Msgf(" - GET %s/api/v1/hooks - List hooks (requires auth)", addr)
|
||||||
|
log.Info().Msgf(" - GET %s/health - Health check", addr)
|
||||||
|
|
||||||
|
// Start the server (blocking call)
|
||||||
|
if err := server.Start(); err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("API server error")
|
||||||
|
}
|
||||||
|
}
|
||||||
73
go.mod
73
go.mod
@@ -3,12 +3,23 @@ module git.warky.dev/wdevs/whatshooked
|
|||||||
go 1.25.5
|
go 1.25.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/bitechdev/ResolveSpec v1.0.49
|
||||||
github.com/eclipse/paho.mqtt.golang v1.5.1
|
github.com/eclipse/paho.mqtt.golang v1.5.1
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/mattn/go-sqlite3 v1.14.32
|
github.com/mattn/go-sqlite3 v1.14.33
|
||||||
github.com/mdp/qrterminal/v3 v3.2.1
|
github.com/mdp/qrterminal/v3 v3.2.1
|
||||||
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
|
github.com/uptrace/bun v1.2.16
|
||||||
|
github.com/uptrace/bun/dialect/pgdialect v1.2.16
|
||||||
|
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16
|
||||||
|
github.com/uptrace/bun/driver/pgdriver v1.2.16
|
||||||
|
github.com/uptrace/bun/driver/sqliteshim v1.2.16
|
||||||
|
github.com/uptrace/bun/extra/bundebug v1.2.16
|
||||||
go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32
|
go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32
|
||||||
golang.org/x/crypto v0.46.0
|
golang.org/x/crypto v0.46.0
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
@@ -18,32 +29,86 @@ require (
|
|||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/beeper/argo-go v1.1.2 // indirect
|
github.com/beeper/argo-go v1.1.2 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/coder/websocket v1.8.14 // indirect
|
github.com/coder/websocket v1.8.14 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
|
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
|
||||||
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/getsentry/sentry-go v0.40.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.8.0 // 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/klauspost/compress v1.18.2 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/microsoft/go-mssqldb v1.9.5 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a // indirect
|
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a // indirect
|
||||||
github.com/rs/zerolog v1.34.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/prometheus/common v0.67.4 // indirect
|
||||||
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||||
|
github.com/redis/go-redis/v9 v9.17.2 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||||
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/tidwall/gjson v1.18.0 // indirect
|
||||||
|
github.com/tidwall/match v1.2.0 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
|
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||||
|
github.com/uptrace/bun/dialect/mssqldialect v1.2.16 // indirect
|
||||||
|
github.com/uptrace/bunrouter v1.0.23 // indirect
|
||||||
github.com/vektah/gqlparser/v2 v2.5.31 // indirect
|
github.com/vektah/gqlparser/v2 v2.5.31 // indirect
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
go.mau.fi/libsignal v0.2.1 // indirect
|
go.mau.fi/libsignal v0.2.1 // indirect
|
||||||
go.mau.fi/util v0.9.4 // indirect
|
go.mau.fi/util v0.9.4 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
go.uber.org/zap v1.27.1 // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
|
||||||
|
golang.org/x/mod v0.31.0 // indirect
|
||||||
golang.org/x/net v0.48.0 // indirect
|
golang.org/x/net v0.48.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.34.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
golang.org/x/term v0.38.0 // indirect
|
golang.org/x/term v0.38.0 // indirect
|
||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
gorm.io/driver/postgres v1.6.0 // indirect
|
||||||
|
gorm.io/driver/sqlite v1.6.0 // indirect
|
||||||
|
gorm.io/driver/sqlserver v1.6.3 // indirect
|
||||||
|
gorm.io/gorm v1.31.1 // indirect
|
||||||
|
mellium.im/sasl v0.3.2 // indirect
|
||||||
|
modernc.org/libc v1.67.4 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
modernc.org/sqlite v1.42.2 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
425
go.sum
425
go.sum
@@ -1,44 +1,182 @@
|
|||||||
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
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/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
|
||||||
|
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/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
|
||||||
|
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/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
|
||||||
|
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/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
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/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
||||||
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||||
github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs=
|
github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs=
|
||||||
github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4=
|
github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bitechdev/ResolveSpec v1.0.48 h1:llXELDnzhcKC6YfRt9vRDKtW8hH3FaoDGWFObjMwfUw=
|
||||||
|
github.com/bitechdev/ResolveSpec v1.0.48/go.mod h1:sEeRZxpZvYS2zQ7RPcLqpRKw5iprmroYeMwUdxe/7to=
|
||||||
|
github.com/bitechdev/ResolveSpec v1.0.49 h1:Ij1JQYElUU2UixB/FqPWC4MDRIJCxlTIz4cZptdvIHI=
|
||||||
|
github.com/bitechdev/ResolveSpec v1.0.49/go.mod h1:sEeRZxpZvYS2zQ7RPcLqpRKw5iprmroYeMwUdxe/7to=
|
||||||
|
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I=
|
||||||
|
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||||
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
|
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||||
|
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||||
|
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
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/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
||||||
|
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
|
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||||
|
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
|
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
|
||||||
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
|
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
|
||||||
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
|
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
|
||||||
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
|
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
|
||||||
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo=
|
||||||
|
github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
|
||||||
|
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||||
|
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||||
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
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.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1/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/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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
|
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.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
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.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
||||||
|
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
||||||
|
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/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
|
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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
|
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||||
|
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
@@ -46,19 +184,72 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
|||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
||||||
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
|
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
|
||||||
|
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
|
||||||
|
github.com/microsoft/go-mssqldb v1.9.5 h1:orwya0X/5bsL1o+KasupTkk2eNTNFkTQG0BEe/HxCn0=
|
||||||
|
github.com/microsoft/go-mssqldb v1.9.5/go.mod h1:VCP2a0KEZZtGLRHd1PsLavLFYy/3xX2yJUPycv3Sr2Q=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
||||||
|
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
|
||||||
|
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||||
|
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||||
|
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||||
|
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||||
|
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||||
|
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||||
|
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||||
|
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||||
|
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||||
|
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||||
|
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/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
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/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a h1:VweslR2akb/ARhXfqSfRbj1vpWwYXf3eeAUyw/ndms0=
|
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a h1:VweslR2akb/ARhXfqSfRbj1vpWwYXf3eeAUyw/ndms0=
|
||||||
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||||
|
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||||
|
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||||
|
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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
|
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
||||||
|
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
||||||
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
|
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
||||||
|
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
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 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
@@ -67,6 +258,12 @@ github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88ee
|
|||||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||||
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
@@ -78,43 +275,261 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
|||||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
|
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.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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
|
||||||
|
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
|
||||||
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
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/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
|
||||||
|
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||||
|
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||||
|
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||||
|
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
|
||||||
|
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
|
||||||
|
github.com/uptrace/bun v1.2.16 h1:QlObi6ZIK5Ao7kAALnh91HWYNZUBbVwye52fmlQM9kc=
|
||||||
|
github.com/uptrace/bun v1.2.16/go.mod h1:jMoNg2n56ckaawi/O/J92BHaECmrz6IRjuMWqlMaMTM=
|
||||||
|
github.com/uptrace/bun/dialect/mssqldialect v1.2.16 h1:rKv0cKPNBviXadB/+2Y/UedA/c1JnwGzUWZkdN5FdSQ=
|
||||||
|
github.com/uptrace/bun/dialect/mssqldialect v1.2.16/go.mod h1:J5U7tGKWDsx2Q7MwDZF2417jCdpD6yD/ZMFJcCR80bk=
|
||||||
|
github.com/uptrace/bun/dialect/pgdialect v1.2.16 h1:KFNZ0LxAyczKNfK/IJWMyaleO6eI9/Z5tUv3DE1NVL4=
|
||||||
|
github.com/uptrace/bun/dialect/pgdialect v1.2.16/go.mod h1:IJdMeV4sLfh0LDUZl7TIxLI0LipF1vwTK3hBC7p5qLo=
|
||||||
|
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16 h1:6wVAiYLj1pMibRthGwy4wDLa3D5AQo32Y8rvwPd8CQ0=
|
||||||
|
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16/go.mod h1:Z7+5qK8CGZkDQiPMu+LSdVuDuR1I5jcwtkB1Pi3F82E=
|
||||||
|
github.com/uptrace/bun/driver/pgdriver v1.2.16 h1:b1kpXKUxtTSGYow5Vlsb+dKV3z0R7aSAJNfMfKp61ZU=
|
||||||
|
github.com/uptrace/bun/driver/pgdriver v1.2.16/go.mod h1:H6lUZ9CBfp1X5Vq62YGSV7q96/v94ja9AYFjKvdoTk0=
|
||||||
|
github.com/uptrace/bun/driver/sqliteshim v1.2.16 h1:M6Dh5kkDWFbUWBrOsIE1g1zdZ5JbSytTD4piFRBOUAI=
|
||||||
|
github.com/uptrace/bun/driver/sqliteshim v1.2.16/go.mod h1:iKdJ06P3XS+pwKcONjSIK07bbhksH3lWsw3mpfr0+bY=
|
||||||
|
github.com/uptrace/bun/extra/bundebug v1.2.16 h1:3OXAfHTU4ydu2+4j05oB1BxPx6+ypdWIVzTugl/7zl0=
|
||||||
|
github.com/uptrace/bun/extra/bundebug v1.2.16/go.mod h1:vk6R/1i67/S2RvUI5AH/m3P5e67mOkfDCmmCsAPUumo=
|
||||||
|
github.com/uptrace/bunrouter v1.0.23 h1:Bi7NKw3uCQkcA/GUCtDNPq5LE5UdR9pe+UyWbjHB/wU=
|
||||||
|
github.com/uptrace/bunrouter v1.0.23/go.mod h1:O3jAcl+5qgnF+ejhgkmbceEk0E/mqaK+ADOocdNpY8M=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k=
|
github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
|
github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
|
go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
|
||||||
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
|
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
|
||||||
go.mau.fi/util v0.9.4 h1:gWdUff+K2rCynRPysXalqqQyr2ahkSWaestH6YhSpso=
|
go.mau.fi/util v0.9.4 h1:gWdUff+K2rCynRPysXalqqQyr2ahkSWaestH6YhSpso=
|
||||||
go.mau.fi/util v0.9.4/go.mod h1:647nVfwUvuhlZFOnro3aRNPmRd2y3iDha9USb8aKSmM=
|
go.mau.fi/util v0.9.4/go.mod h1:647nVfwUvuhlZFOnro3aRNPmRd2y3iDha9USb8aKSmM=
|
||||||
go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32 h1:NeE9eEYY4kEJVCfCXaAU27LgAPugPHRHJdC9IpXFPzI=
|
go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32 h1:NeE9eEYY4kEJVCfCXaAU27LgAPugPHRHJdC9IpXFPzI=
|
||||||
go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32/go.mod h1:S4OWR9+hTx+54+jRzl+NfRBXnGpPm5IRPyhXB7haSd0=
|
go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32/go.mod h1:S4OWR9+hTx+54+jRzl+NfRBXnGpPm5IRPyhXB7haSd0=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||||
|
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||||
|
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||||
|
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||||
|
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
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.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||||
|
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.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||||
|
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||||
|
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-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.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||||
|
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
|
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
|
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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
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-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.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.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.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.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
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/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||||
|
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
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.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
|
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.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||||
|
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
|
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||||
|
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||||
|
gorm.io/driver/sqlserver v1.6.3 h1:UR+nWCuphPnq7UxnL57PSrlYjuvs+sf1N59GgFX7uAI=
|
||||||
|
gorm.io/driver/sqlserver v1.6.3/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U=
|
||||||
|
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
|
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||||
|
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
|
mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0=
|
||||||
|
mellium.im/sasl v0.3.2/go.mod h1:NKXDi1zkr+BlMHLQjY3ofYuU4KSPFxknb8mfEu6SveY=
|
||||||
|
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||||
|
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
|
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||||
|
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||||
|
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||||
|
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||||
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||||
|
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
|
modernc.org/libc v1.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg=
|
||||||
|
modernc.org/libc v1.67.4/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.42.2 h1:7hkZUNJvJFN2PgfUdjni9Kbvd4ef4mNLOu0B9FGxM74=
|
||||||
|
modernc.org/sqlite v1.42.2/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||||
|
|||||||
271
pkg/api/security.go
Normal file
271
pkg/api/security.go
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/config"
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/storage"
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/security"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SecurityProvider implements ResolveSpec SecurityProvider interface
|
||||||
|
type SecurityProvider struct {
|
||||||
|
jwtSecret []byte
|
||||||
|
userRepo *storage.UserRepository
|
||||||
|
sessionRepo *storage.SessionRepository
|
||||||
|
config *config.Config // Add config for Phase 1 auth
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSecurityProvider creates a new security provider
|
||||||
|
func NewSecurityProvider(jwtSecret string, db *bun.DB, cfg *config.Config) security.SecurityProvider {
|
||||||
|
return &SecurityProvider{
|
||||||
|
jwtSecret: []byte(jwtSecret),
|
||||||
|
userRepo: storage.NewUserRepository(db),
|
||||||
|
sessionRepo: storage.NewSessionRepository(db),
|
||||||
|
config: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claims represents JWT claims
|
||||||
|
type Claims struct {
|
||||||
|
UserID int `json:"user_id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateToken generates a JWT token
|
||||||
|
func (sp *SecurityProvider) GenerateToken(userID int, username, role string) (string, error) {
|
||||||
|
expirationTime := time.Now().Add(24 * time.Hour)
|
||||||
|
|
||||||
|
claims := &Claims{
|
||||||
|
UserID: userID,
|
||||||
|
Username: username,
|
||||||
|
Role: role,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||||
|
Issuer: "whatshooked",
|
||||||
|
Subject: fmt.Sprintf("%d", userID),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString(sp.jwtSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateToken validates a JWT token and returns the claims
|
||||||
|
func (sp *SecurityProvider) ValidateToken(tokenString string) (*Claims, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
return sp.jwtSecret, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("invalid token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login authenticates a user (implements security.Authenticator)
|
||||||
|
func (sp *SecurityProvider) Login(ctx context.Context, req security.LoginRequest) (*security.LoginResponse, error) {
|
||||||
|
// Get user by username
|
||||||
|
user, err := sp.userRepo.GetByUsername(ctx, req.Username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is active
|
||||||
|
if !user.Active {
|
||||||
|
return nil, fmt.Errorf("user is inactive")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify password
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(user.Password.String()), []byte(req.Password)); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate JWT token
|
||||||
|
token, err := sp.GenerateToken(int(user.ID.Int64()), req.Username, user.Role.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build user context
|
||||||
|
userCtx := &security.UserContext{
|
||||||
|
UserID: int(user.ID.Int64()),
|
||||||
|
UserName: req.Username,
|
||||||
|
Email: user.Email.String(),
|
||||||
|
Roles: []string{user.Role.String()},
|
||||||
|
Claims: map[string]any{
|
||||||
|
"role": user.Role.String(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &security.LoginResponse{
|
||||||
|
Token: token,
|
||||||
|
User: userCtx,
|
||||||
|
ExpiresIn: int64(24 * time.Hour.Seconds()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout logs out a user (implements security.Authenticator)
|
||||||
|
func (sp *SecurityProvider) Logout(ctx context.Context, req security.LogoutRequest) error {
|
||||||
|
// For JWT, we can implement token blacklisting if needed
|
||||||
|
// For now, just return success (JWT will expire naturally)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate authenticates an HTTP request (implements security.Authenticator)
|
||||||
|
func (sp *SecurityProvider) Authenticate(r *http.Request) (*security.UserContext, error) {
|
||||||
|
// Try JWT authentication first
|
||||||
|
token := extractBearerToken(r)
|
||||||
|
if token != "" {
|
||||||
|
claims, err := sp.ValidateToken(token)
|
||||||
|
if err == nil {
|
||||||
|
// Get user from database
|
||||||
|
user, err := sp.userRepo.GetByID(r.Context(), fmt.Sprintf("%d", claims.UserID))
|
||||||
|
if err == nil && user.Active {
|
||||||
|
return &security.UserContext{
|
||||||
|
UserID: claims.UserID,
|
||||||
|
UserName: claims.Username,
|
||||||
|
Email: user.Email.String(),
|
||||||
|
Roles: []string{user.Role.String()},
|
||||||
|
Claims: map[string]any{
|
||||||
|
"role": user.Role.String(),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Phase 1 authentication (API key, basic auth)
|
||||||
|
if sp.validatePhase1Auth(r) {
|
||||||
|
// Create a generic user context for Phase 1 auth
|
||||||
|
// Use username from config or "api-user" if using API key
|
||||||
|
username := "api-user"
|
||||||
|
if sp.config.Server.Username != "" {
|
||||||
|
username = sp.config.Server.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if using basic auth to get actual username
|
||||||
|
if basicUser, _, ok := r.BasicAuth(); ok && basicUser != "" {
|
||||||
|
username = basicUser
|
||||||
|
}
|
||||||
|
|
||||||
|
return &security.UserContext{
|
||||||
|
UserID: 0, // No user ID for Phase 1 auth
|
||||||
|
UserName: username,
|
||||||
|
Email: "",
|
||||||
|
Roles: []string{"admin"}, // Phase 1 auth gets admin role
|
||||||
|
Claims: map[string]any{
|
||||||
|
"role": "admin",
|
||||||
|
"legacy": true, // Mark as legacy auth
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("authentication failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// validatePhase1Auth checks Phase 1 authentication (API key or basic auth)
|
||||||
|
func (sp *SecurityProvider) validatePhase1Auth(r *http.Request) bool {
|
||||||
|
// Check if any Phase 1 authentication is configured
|
||||||
|
hasAuth := sp.config.Server.AuthKey != "" ||
|
||||||
|
sp.config.Server.Username != "" ||
|
||||||
|
sp.config.Server.Password != ""
|
||||||
|
|
||||||
|
if !hasAuth {
|
||||||
|
// No Phase 1 authentication configured
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for API key authentication (x-api-key header)
|
||||||
|
if sp.config.Server.AuthKey != "" {
|
||||||
|
apiKey := r.Header.Get("x-api-key")
|
||||||
|
if apiKey == sp.config.Server.AuthKey {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check Authorization header for bearer token (API key)
|
||||||
|
authHeader := r.Header.Get("Authorization")
|
||||||
|
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
|
||||||
|
token := authHeader[7:]
|
||||||
|
if token == sp.config.Server.AuthKey {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for username/password authentication (HTTP Basic Auth)
|
||||||
|
if sp.config.Server.Username != "" && sp.config.Server.Password != "" {
|
||||||
|
username, password, ok := r.BasicAuth()
|
||||||
|
if ok && username == sp.config.Server.Username && password == sp.config.Server.Password {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColumnSecurity returns column security rules (implements security.ColumnSecurityProvider)
|
||||||
|
func (sp *SecurityProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]security.ColumnSecurity, error) {
|
||||||
|
// Return empty - no column-level security for now
|
||||||
|
return []security.ColumnSecurity{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRowSecurity returns row security rules (implements security.RowSecurityProvider)
|
||||||
|
func (sp *SecurityProvider) GetRowSecurity(ctx context.Context, userID int, schema, table string) (security.RowSecurity, error) {
|
||||||
|
// Get user to check role
|
||||||
|
user, err := sp.userRepo.GetByID(ctx, fmt.Sprintf("%d", userID))
|
||||||
|
if err != nil {
|
||||||
|
return security.RowSecurity{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin can access all rows
|
||||||
|
if user.Role.String() == "admin" {
|
||||||
|
return security.RowSecurity{
|
||||||
|
Template: "", // Empty template means no filtering
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular users can only access their own data
|
||||||
|
// Apply user_id filter for tables that have user_id column
|
||||||
|
userTables := []string{"api_keys", "hooks", "whatsapp_accounts"}
|
||||||
|
for _, userTable := range userTables {
|
||||||
|
if table == userTable {
|
||||||
|
return security.RowSecurity{
|
||||||
|
Template: fmt.Sprintf("user_id = %d", userID),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other tables, no additional filtering
|
||||||
|
return security.RowSecurity{
|
||||||
|
Template: "",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractBearerToken extracts Bearer token from Authorization header
|
||||||
|
func extractBearerToken(r *http.Request) string {
|
||||||
|
auth := r.Header.Get("Authorization")
|
||||||
|
if len(auth) > 7 && strings.HasPrefix(auth, "Bearer ") {
|
||||||
|
return auth[7:]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
343
pkg/api/server.go
Normal file
343
pkg/api/server.go
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/config"
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/handlers"
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/models"
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/storage"
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/database"
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/modelregistry"
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/restheadspec"
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/security"
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/server"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WhatsHookedInterface defines the interface for accessing WhatsHooked components
|
||||||
|
type WhatsHookedInterface interface {
|
||||||
|
Handlers() *handlers.Handlers
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server represents the API server
|
||||||
|
type Server struct {
|
||||||
|
serverMgr server.Manager
|
||||||
|
handler *restheadspec.Handler
|
||||||
|
secProvider security.SecurityProvider
|
||||||
|
db *bun.DB
|
||||||
|
config *config.Config
|
||||||
|
wh WhatsHookedInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServer creates a new API server with ResolveSpec integration
|
||||||
|
func NewServer(cfg *config.Config, db *bun.DB, wh WhatsHookedInterface) (*Server, error) {
|
||||||
|
// Create model registry and register models
|
||||||
|
registry := modelregistry.NewModelRegistry()
|
||||||
|
registerModelsToRegistry(registry)
|
||||||
|
|
||||||
|
// Create BUN adapter
|
||||||
|
bunAdapter := database.NewBunAdapter(db)
|
||||||
|
|
||||||
|
// Create ResolveSpec handler with registry
|
||||||
|
handler := restheadspec.NewHandler(bunAdapter, registry)
|
||||||
|
|
||||||
|
// Create security provider
|
||||||
|
secProvider := NewSecurityProvider(cfg.Server.JWTSecret, db, cfg)
|
||||||
|
|
||||||
|
// Create security list and register hooks
|
||||||
|
securityList, err := security.NewSecurityList(secProvider)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create security list: %w", err)
|
||||||
|
}
|
||||||
|
restheadspec.RegisterSecurityHooks(handler, securityList)
|
||||||
|
|
||||||
|
// Create router
|
||||||
|
router := mux.NewRouter()
|
||||||
|
|
||||||
|
// Create a subrouter for /api/v1/* routes that need JWT authentication
|
||||||
|
apiV1Router := router.PathPrefix("/api/v1").Subrouter()
|
||||||
|
apiV1Router.Use(security.NewAuthMiddleware(securityList))
|
||||||
|
apiV1Router.Use(security.SetSecurityMiddleware(securityList))
|
||||||
|
|
||||||
|
// Setup WhatsApp API routes on main router (these use their own Auth middleware)
|
||||||
|
SetupWhatsAppRoutes(router, wh)
|
||||||
|
|
||||||
|
// Setup ResolveSpec routes on the protected /api/v1 subrouter (auto-generated CRUD)
|
||||||
|
restheadspec.SetupMuxRoutes(apiV1Router, handler, nil)
|
||||||
|
|
||||||
|
// Add custom routes (login, logout, etc.) on main router
|
||||||
|
SetupCustomRoutes(router, secProvider, db)
|
||||||
|
|
||||||
|
// Add static file serving for React app (must be last - catch-all route)
|
||||||
|
// Serve React app from web/dist directory
|
||||||
|
spa := spaHandler{staticPath: "web/dist", indexPath: "index.html"}
|
||||||
|
router.PathPrefix("/").Handler(spa)
|
||||||
|
|
||||||
|
// Create server manager
|
||||||
|
serverMgr := server.NewManager()
|
||||||
|
|
||||||
|
// Add HTTP server
|
||||||
|
_, err = serverMgr.Add(server.Config{
|
||||||
|
Name: "whatshooked",
|
||||||
|
Host: cfg.Server.Host,
|
||||||
|
Port: cfg.Server.Port,
|
||||||
|
Handler: router,
|
||||||
|
GZIP: true,
|
||||||
|
ShutdownTimeout: 30 * time.Second,
|
||||||
|
DrainTimeout: 25 * time.Second,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to add server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register shutdown callback for database
|
||||||
|
serverMgr.RegisterShutdownCallback(func(ctx context.Context) error {
|
||||||
|
return storage.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
return &Server{
|
||||||
|
serverMgr: serverMgr,
|
||||||
|
handler: handler,
|
||||||
|
secProvider: secProvider,
|
||||||
|
db: db,
|
||||||
|
config: cfg,
|
||||||
|
wh: wh,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the API server
|
||||||
|
func (s *Server) Start() error {
|
||||||
|
return s.serverMgr.ServeWithGracefulShutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerModelsToRegistry registers all BUN models with the model registry
|
||||||
|
func registerModelsToRegistry(registry common.ModelRegistry) {
|
||||||
|
// Register all models with their table names (without schema for SQLite compatibility)
|
||||||
|
registry.RegisterModel("users", &models.ModelPublicUser{})
|
||||||
|
registry.RegisterModel("api_keys", &models.ModelPublicAPIKey{})
|
||||||
|
registry.RegisterModel("hooks", &models.ModelPublicHook{})
|
||||||
|
registry.RegisterModel("whatsapp_accounts", &models.ModelPublicWhatsappAccount{})
|
||||||
|
registry.RegisterModel("event_logs", &models.ModelPublicEventLog{})
|
||||||
|
registry.RegisterModel("sessions", &models.ModelPublicSession{})
|
||||||
|
registry.RegisterModel("message_cache", &models.ModelPublicMessageCache{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupWhatsAppRoutes adds all WhatsApp API routes
|
||||||
|
func SetupWhatsAppRoutes(router *mux.Router, wh WhatsHookedInterface) {
|
||||||
|
h := wh.Handlers()
|
||||||
|
|
||||||
|
// Landing page (no auth required)
|
||||||
|
router.HandleFunc("/", h.ServeIndex).Methods("GET")
|
||||||
|
|
||||||
|
// Privacy policy and terms of service (no auth required)
|
||||||
|
router.HandleFunc("/privacy-policy", h.ServePrivacyPolicy).Methods("GET")
|
||||||
|
router.HandleFunc("/terms-of-service", h.ServeTermsOfService).Methods("GET")
|
||||||
|
|
||||||
|
// Static files (no auth required)
|
||||||
|
router.PathPrefix("/static/").HandlerFunc(h.ServeStatic)
|
||||||
|
|
||||||
|
// Health check (no auth required)
|
||||||
|
router.HandleFunc("/health", h.Health).Methods("GET")
|
||||||
|
|
||||||
|
// Hook management (with auth)
|
||||||
|
router.HandleFunc("/api/hooks", h.Auth(h.Hooks))
|
||||||
|
router.HandleFunc("/api/hooks/add", h.Auth(h.AddHook)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/hooks/remove", h.Auth(h.RemoveHook)).Methods("POST")
|
||||||
|
|
||||||
|
// Account management (with auth)
|
||||||
|
router.HandleFunc("/api/accounts", h.Auth(h.Accounts))
|
||||||
|
router.HandleFunc("/api/accounts/add", h.Auth(h.AddAccount)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/accounts/update", h.Auth(h.UpdateAccount)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/accounts/remove", h.Auth(h.RemoveAccount)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/accounts/disable", h.Auth(h.DisableAccount)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/accounts/enable", h.Auth(h.EnableAccount)).Methods("POST")
|
||||||
|
|
||||||
|
// Send messages (with auth)
|
||||||
|
router.HandleFunc("/api/send", h.Auth(h.SendMessage)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/send/image", h.Auth(h.SendImage)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/send/video", h.Auth(h.SendVideo)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/send/document", h.Auth(h.SendDocument)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/send/audio", h.Auth(h.SendAudio)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/send/sticker", h.Auth(h.SendSticker)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/send/location", h.Auth(h.SendLocation)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/send/contacts", h.Auth(h.SendContacts)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/send/interactive", h.Auth(h.SendInteractive)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/send/template", h.Auth(h.SendTemplate)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/send/flow", h.Auth(h.SendFlow)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/send/reaction", h.Auth(h.SendReaction)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/send/catalog", h.Auth(h.SendCatalogMessage)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/send/product", h.Auth(h.SendSingleProduct)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/send/product-list", h.Auth(h.SendProductList)).Methods("POST")
|
||||||
|
|
||||||
|
// Message operations (with auth)
|
||||||
|
router.HandleFunc("/api/messages/read", h.Auth(h.MarkAsRead)).Methods("POST")
|
||||||
|
|
||||||
|
// Serve media files (with auth)
|
||||||
|
router.PathPrefix("/api/media/").HandlerFunc(h.ServeMedia)
|
||||||
|
|
||||||
|
// Serve QR codes (no auth - needed during pairing)
|
||||||
|
router.PathPrefix("/api/qr/").HandlerFunc(h.ServeQRCode)
|
||||||
|
|
||||||
|
// Business API webhooks (no auth - Meta validates via verify_token)
|
||||||
|
router.PathPrefix("/webhooks/whatsapp/").HandlerFunc(h.BusinessAPIWebhook)
|
||||||
|
|
||||||
|
// Template management (with auth)
|
||||||
|
router.HandleFunc("/api/templates", h.Auth(h.ListTemplates))
|
||||||
|
router.HandleFunc("/api/templates/upload", h.Auth(h.UploadTemplate)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/templates/delete", h.Auth(h.DeleteTemplate)).Methods("POST")
|
||||||
|
|
||||||
|
// Flow management (with auth)
|
||||||
|
router.HandleFunc("/api/flows", h.Auth(h.ListFlows))
|
||||||
|
router.HandleFunc("/api/flows/create", h.Auth(h.CreateFlow)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/flows/get", h.Auth(h.GetFlow))
|
||||||
|
router.HandleFunc("/api/flows/upload", h.Auth(h.UploadFlowAsset)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/flows/publish", h.Auth(h.PublishFlow)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/flows/deprecate", h.Auth(h.DeprecateFlow)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/flows/delete", h.Auth(h.DeleteFlow)).Methods("POST")
|
||||||
|
|
||||||
|
// Phone number management (with auth)
|
||||||
|
router.HandleFunc("/api/phone-numbers", h.Auth(h.ListPhoneNumbers))
|
||||||
|
router.HandleFunc("/api/phone-numbers/request-code", h.Auth(h.RequestVerificationCode)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/phone-numbers/verify-code", h.Auth(h.VerifyCode)).Methods("POST")
|
||||||
|
|
||||||
|
// Media management (with auth)
|
||||||
|
router.HandleFunc("/api/media/upload", h.Auth(h.UploadMedia)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/media-delete", h.Auth(h.DeleteMediaFile)).Methods("POST")
|
||||||
|
|
||||||
|
// Business profile (with auth)
|
||||||
|
router.HandleFunc("/api/business-profile", h.Auth(h.GetBusinessProfile))
|
||||||
|
router.HandleFunc("/api/business-profile/update", h.Auth(h.UpdateBusinessProfile)).Methods("POST")
|
||||||
|
|
||||||
|
// Catalog / commerce (with auth)
|
||||||
|
router.HandleFunc("/api/catalogs", h.Auth(h.ListCatalogs))
|
||||||
|
router.HandleFunc("/api/catalogs/products", h.Auth(h.ListProducts))
|
||||||
|
|
||||||
|
// Message cache management (with auth)
|
||||||
|
router.HandleFunc("/api/cache", h.Auth(h.GetCachedEvents)).Methods("GET")
|
||||||
|
router.HandleFunc("/api/cache/stats", h.Auth(h.GetCacheStats)).Methods("GET")
|
||||||
|
router.HandleFunc("/api/cache/replay", h.Auth(h.ReplayCachedEvents)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/cache/event", h.Auth(h.GetCachedEvent)).Methods("GET")
|
||||||
|
router.HandleFunc("/api/cache/event/replay", h.Auth(h.ReplayCachedEvent)).Methods("POST")
|
||||||
|
router.HandleFunc("/api/cache/event/delete", h.Auth(h.DeleteCachedEvent)).Methods("DELETE")
|
||||||
|
router.HandleFunc("/api/cache/clear", h.Auth(h.ClearCache)).Methods("DELETE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupCustomRoutes adds custom authentication and management routes
|
||||||
|
func SetupCustomRoutes(router *mux.Router, secProvider security.SecurityProvider, db *bun.DB) {
|
||||||
|
// Health check endpoint
|
||||||
|
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"status":"healthy"}`))
|
||||||
|
}).Methods("GET")
|
||||||
|
|
||||||
|
// Login endpoint
|
||||||
|
router.HandleFunc("/api/v1/auth/login", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
handleLogin(w, r, secProvider)
|
||||||
|
}).Methods("POST")
|
||||||
|
|
||||||
|
// Logout endpoint
|
||||||
|
router.HandleFunc("/api/v1/auth/logout", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
handleLogout(w, r, secProvider)
|
||||||
|
}).Methods("POST")
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleLogin handles user login
|
||||||
|
func handleLogin(w http.ResponseWriter, r *http.Request, secProvider security.SecurityProvider) {
|
||||||
|
var req security.LoginRequest
|
||||||
|
if err := parseJSON(r, &req); err != nil {
|
||||||
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := secProvider.Login(r.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleLogout handles user logout
|
||||||
|
func handleLogout(w http.ResponseWriter, r *http.Request, secProvider security.SecurityProvider) {
|
||||||
|
token := extractToken(r)
|
||||||
|
if token == "" {
|
||||||
|
http.Error(w, "No token provided", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := security.LogoutRequest{Token: token}
|
||||||
|
if err := secProvider.Logout(r.Context(), req); err != nil {
|
||||||
|
http.Error(w, "Logout failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, map[string]string{"message": "Logged out successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
func parseJSON(r *http.Request, v interface{}) error {
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
return decoder.Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
json.NewEncoder(w).Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractToken(r *http.Request) string {
|
||||||
|
// Extract from Authorization header: "Bearer <token>"
|
||||||
|
auth := r.Header.Get("Authorization")
|
||||||
|
if len(auth) > 7 && auth[:7] == "Bearer " {
|
||||||
|
return auth[7:]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// spaHandler implements the http.Handler interface for serving a SPA
|
||||||
|
type spaHandler struct {
|
||||||
|
staticPath string
|
||||||
|
indexPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP inspects the URL path to locate a file within the static dir
|
||||||
|
// If a file is found, it is served. If not, the index.html file is served for client-side routing
|
||||||
|
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Get the path
|
||||||
|
path := r.URL.Path
|
||||||
|
|
||||||
|
// Check whether a file exists at the given path
|
||||||
|
info, err := http.Dir(h.staticPath).Open(path)
|
||||||
|
if err != nil {
|
||||||
|
// File does not exist, serve index.html for client-side routing
|
||||||
|
http.ServeFile(w, r, h.staticPath+"/"+h.indexPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer info.Close()
|
||||||
|
|
||||||
|
// Check if path is a directory
|
||||||
|
stat, err := info.Stat()
|
||||||
|
if err != nil {
|
||||||
|
http.ServeFile(w, r, h.staticPath+"/"+h.indexPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat.IsDir() {
|
||||||
|
// Serve index.html for directories
|
||||||
|
http.ServeFile(w, r, h.staticPath+"/"+h.indexPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, serve the file
|
||||||
|
http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r)
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ type ServerConfig struct {
|
|||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
AuthKey string `json:"auth_key,omitempty"`
|
AuthKey string `json:"auth_key,omitempty"`
|
||||||
|
JWTSecret string `json:"jwt_secret,omitempty"` // Secret for JWT signing
|
||||||
TLS TLSConfig `json:"tls,omitempty"`
|
TLS TLSConfig `json:"tls,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +153,10 @@ func Load(path string) (*Config, error) {
|
|||||||
cfg.Server.Host = "0.0.0.0"
|
cfg.Server.Host = "0.0.0.0"
|
||||||
}
|
}
|
||||||
if cfg.Server.Port == 0 {
|
if cfg.Server.Port == 0 {
|
||||||
cfg.Server.Port = 8825
|
cfg.Server.Port = 8080
|
||||||
|
}
|
||||||
|
if cfg.Server.JWTSecret == "" {
|
||||||
|
cfg.Server.JWTSecret = "change-me-in-production" // Default for development
|
||||||
}
|
}
|
||||||
if cfg.Media.DataPath == "" {
|
if cfg.Media.DataPath == "" {
|
||||||
cfg.Media.DataPath = "./data/media"
|
cfg.Media.DataPath = "./data/media"
|
||||||
|
|||||||
70
pkg/models/sql_public_api_keys.go
Normal file
70
pkg/models/sql_public_api_keys.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// Code generated by relspecgo. DO NOT EDIT.
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelPublicAPIKey struct {
|
||||||
|
bun.BaseModel `bun:"table:api_keys,alias:api_keys"`
|
||||||
|
ID resolvespec_common.SqlString `bun:"id,type:varchar(36),pk," json:"id"` // UUID
|
||||||
|
Active bool `bun:"active,type:boolean,default:true,notnull," json:"active"`
|
||||||
|
CreatedAt resolvespec_common.SqlTime `bun:"created_at,type:timestamp,default:now(),notnull," json:"created_at"`
|
||||||
|
DeletedAt resolvespec_common.SqlTime `bun:"deleted_at,type:timestamp,nullzero," json:"deleted_at"`
|
||||||
|
ExpiresAt resolvespec_common.SqlTime `bun:"expires_at,type:timestamp,nullzero," json:"expires_at"`
|
||||||
|
Key resolvespec_common.SqlString `bun:"key,type:varchar(255),notnull," json:"key"` // Hashed API key
|
||||||
|
KeyPrefix resolvespec_common.SqlString `bun:"key_prefix,type:varchar(20),nullzero," json:"key_prefix"` // First few characters for display
|
||||||
|
LastUsedAt resolvespec_common.SqlTime `bun:"last_used_at,type:timestamp,nullzero," json:"last_used_at"`
|
||||||
|
Name resolvespec_common.SqlString `bun:"name,type:varchar(255),notnull," json:"name"` // Friendly name for the API key
|
||||||
|
Permissions resolvespec_common.SqlString `bun:"permissions,type:text,nullzero," json:"permissions"` // JSON array of permissions
|
||||||
|
UpdatedAt resolvespec_common.SqlTime `bun:"updated_at,type:timestamp,default:now(),notnull," json:"updated_at"`
|
||||||
|
UserID resolvespec_common.SqlString `bun:"user_id,type:varchar(36),notnull," json:"user_id"`
|
||||||
|
RelUserID *ModelPublicUser `bun:"rel:has-one,join:user_id=id" json:"reluserid,omitempty"` // Has one ModelPublicUser
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the table name for ModelPublicAPIKey
|
||||||
|
func (m ModelPublicAPIKey) TableName() string {
|
||||||
|
return "api_keys"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableNameOnly returns the table name without schema for ModelPublicAPIKey
|
||||||
|
func (m ModelPublicAPIKey) TableNameOnly() string {
|
||||||
|
return "api_keys"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaName returns the schema name for ModelPublicAPIKey
|
||||||
|
func (m ModelPublicAPIKey) SchemaName() string {
|
||||||
|
return "public"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the primary key value
|
||||||
|
func (m ModelPublicAPIKey) GetID() int64 {
|
||||||
|
return m.ID.Int64()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDStr returns the primary key as a string
|
||||||
|
func (m ModelPublicAPIKey) GetIDStr() string {
|
||||||
|
return fmt.Sprintf("%d", m.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetID sets the primary key value
|
||||||
|
func (m ModelPublicAPIKey) SetID(newid int64) {
|
||||||
|
m.UpdateID(newid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateID updates the primary key value
|
||||||
|
func (m *ModelPublicAPIKey) UpdateID(newid int64) {
|
||||||
|
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDName returns the name of the primary key column
|
||||||
|
func (m ModelPublicAPIKey) GetIDName() string {
|
||||||
|
return "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefix returns the table prefix
|
||||||
|
func (m ModelPublicAPIKey) GetPrefix() string {
|
||||||
|
return "AKP"
|
||||||
|
}
|
||||||
70
pkg/models/sql_public_event_logs.go
Normal file
70
pkg/models/sql_public_event_logs.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// Code generated by relspecgo. DO NOT EDIT.
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelPublicEventLog struct {
|
||||||
|
bun.BaseModel `bun:"table:event_logs,alias:event_logs"`
|
||||||
|
ID resolvespec_common.SqlString `bun:"id,type:varchar(36),pk," json:"id"` // UUID
|
||||||
|
Action resolvespec_common.SqlString `bun:"action,type:varchar(50),nullzero," json:"action"` // create
|
||||||
|
CreatedAt resolvespec_common.SqlTime `bun:"created_at,type:timestamp,default:now(),notnull," json:"created_at"`
|
||||||
|
Data resolvespec_common.SqlString `bun:"data,type:text,nullzero," json:"data"` // JSON encoded event data
|
||||||
|
EntityID resolvespec_common.SqlString `bun:"entity_id,type:varchar(36),nullzero," json:"entity_id"`
|
||||||
|
EntityType resolvespec_common.SqlString `bun:"entity_type,type:varchar(100),nullzero," json:"entity_type"` // user
|
||||||
|
Error resolvespec_common.SqlString `bun:"error,type:text,nullzero," json:"error"`
|
||||||
|
EventType resolvespec_common.SqlString `bun:"event_type,type:varchar(100),notnull," json:"event_type"`
|
||||||
|
IpAddress resolvespec_common.SqlString `bun:"ip_address,type:varchar(50),nullzero," json:"ip_address"`
|
||||||
|
Success bool `bun:"success,type:boolean,default:true,notnull," json:"success"`
|
||||||
|
UserAgent resolvespec_common.SqlString `bun:"user_agent,type:text,nullzero," json:"user_agent"`
|
||||||
|
UserID resolvespec_common.SqlString `bun:"user_id,type:varchar(36),nullzero," json:"user_id"` // Optional user reference
|
||||||
|
RelUserID *ModelPublicUser `bun:"rel:has-one,join:user_id=id" json:"reluserid,omitempty"` // Has one ModelPublicUser
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the table name for ModelPublicEventLog
|
||||||
|
func (m ModelPublicEventLog) TableName() string {
|
||||||
|
return "event_logs"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableNameOnly returns the table name without schema for ModelPublicEventLog
|
||||||
|
func (m ModelPublicEventLog) TableNameOnly() string {
|
||||||
|
return "event_logs"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaName returns the schema name for ModelPublicEventLog
|
||||||
|
func (m ModelPublicEventLog) SchemaName() string {
|
||||||
|
return "public"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the primary key value
|
||||||
|
func (m ModelPublicEventLog) GetID() int64 {
|
||||||
|
return m.ID.Int64()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDStr returns the primary key as a string
|
||||||
|
func (m ModelPublicEventLog) GetIDStr() string {
|
||||||
|
return fmt.Sprintf("%d", m.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetID sets the primary key value
|
||||||
|
func (m ModelPublicEventLog) SetID(newid int64) {
|
||||||
|
m.UpdateID(newid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateID updates the primary key value
|
||||||
|
func (m *ModelPublicEventLog) UpdateID(newid int64) {
|
||||||
|
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDName returns the name of the primary key column
|
||||||
|
func (m ModelPublicEventLog) GetIDName() string {
|
||||||
|
return "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefix returns the table prefix
|
||||||
|
func (m ModelPublicEventLog) GetPrefix() string {
|
||||||
|
return "ELV"
|
||||||
|
}
|
||||||
73
pkg/models/sql_public_hooks.go
Normal file
73
pkg/models/sql_public_hooks.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// Code generated by relspecgo. DO NOT EDIT.
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelPublicHook struct {
|
||||||
|
bun.BaseModel `bun:"table:hooks,alias:hooks"`
|
||||||
|
ID resolvespec_common.SqlString `bun:"id,type:varchar(36),pk," json:"id"` // UUID
|
||||||
|
Active bool `bun:"active,type:boolean,default:true,notnull," json:"active"`
|
||||||
|
CreatedAt resolvespec_common.SqlTime `bun:"created_at,type:timestamp,default:now(),notnull," json:"created_at"`
|
||||||
|
DeletedAt resolvespec_common.SqlTime `bun:"deleted_at,type:timestamp,nullzero," json:"deleted_at"`
|
||||||
|
Description resolvespec_common.SqlString `bun:"description,type:text,nullzero," json:"description"`
|
||||||
|
Events resolvespec_common.SqlString `bun:"events,type:text,nullzero," json:"events"` // JSON array of event types
|
||||||
|
Headers resolvespec_common.SqlString `bun:"headers,type:text,nullzero," json:"headers"` // JSON encoded headers
|
||||||
|
Method resolvespec_common.SqlString `bun:"method,type:varchar(10),default:POST,notnull," json:"method"` // HTTP method
|
||||||
|
Name resolvespec_common.SqlString `bun:"name,type:varchar(255),notnull," json:"name"`
|
||||||
|
RetryCount resolvespec_common.SqlInt32 `bun:"retry_count,type:int,default:3,notnull," json:"retry_count"`
|
||||||
|
Secret resolvespec_common.SqlString `bun:"secret,type:varchar(255),nullzero," json:"secret"` // HMAC signature secret
|
||||||
|
Timeout resolvespec_common.SqlInt32 `bun:"timeout,type:int,default:30,notnull," json:"timeout"` // Timeout in seconds
|
||||||
|
UpdatedAt resolvespec_common.SqlTime `bun:"updated_at,type:timestamp,default:now(),notnull," json:"updated_at"`
|
||||||
|
URL resolvespec_common.SqlString `bun:"url,type:text,notnull," json:"url"`
|
||||||
|
UserID resolvespec_common.SqlString `bun:"user_id,type:varchar(36),notnull," json:"user_id"`
|
||||||
|
RelUserID *ModelPublicUser `bun:"rel:has-one,join:user_id=id" json:"reluserid,omitempty"` // Has one ModelPublicUser
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the table name for ModelPublicHook
|
||||||
|
func (m ModelPublicHook) TableName() string {
|
||||||
|
return "hooks"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableNameOnly returns the table name without schema for ModelPublicHook
|
||||||
|
func (m ModelPublicHook) TableNameOnly() string {
|
||||||
|
return "hooks"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaName returns the schema name for ModelPublicHook
|
||||||
|
func (m ModelPublicHook) SchemaName() string {
|
||||||
|
return "public"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the primary key value
|
||||||
|
func (m ModelPublicHook) GetID() int64 {
|
||||||
|
return m.ID.Int64()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDStr returns the primary key as a string
|
||||||
|
func (m ModelPublicHook) GetIDStr() string {
|
||||||
|
return fmt.Sprintf("%d", m.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetID sets the primary key value
|
||||||
|
func (m ModelPublicHook) SetID(newid int64) {
|
||||||
|
m.UpdateID(newid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateID updates the primary key value
|
||||||
|
func (m *ModelPublicHook) UpdateID(newid int64) {
|
||||||
|
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDName returns the name of the primary key column
|
||||||
|
func (m ModelPublicHook) GetIDName() string {
|
||||||
|
return "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefix returns the table prefix
|
||||||
|
func (m ModelPublicHook) GetPrefix() string {
|
||||||
|
return "HOO"
|
||||||
|
}
|
||||||
66
pkg/models/sql_public_message_cache.go
Normal file
66
pkg/models/sql_public_message_cache.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// Code generated by relspecgo. DO NOT EDIT.
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelPublicMessageCache struct {
|
||||||
|
bun.BaseModel `bun:"table:message_cache,alias:message_cache"`
|
||||||
|
ID resolvespec_common.SqlString `bun:"id,type:varchar(36),pk," json:"id"` // UUID
|
||||||
|
AccountID resolvespec_common.SqlString `bun:"account_id,type:varchar(36),notnull," json:"account_id"`
|
||||||
|
ChatID resolvespec_common.SqlString `bun:"chat_id,type:varchar(255),notnull," json:"chat_id"`
|
||||||
|
Content resolvespec_common.SqlString `bun:"content,type:text,notnull," json:"content"` // JSON encoded message content
|
||||||
|
CreatedAt resolvespec_common.SqlTime `bun:"created_at,type:timestamp,default:now(),notnull," json:"created_at"`
|
||||||
|
FromMe bool `bun:"from_me,type:boolean,notnull," json:"from_me"`
|
||||||
|
MessageID resolvespec_common.SqlString `bun:"message_id,type:varchar(255),notnull," json:"message_id"`
|
||||||
|
MessageType resolvespec_common.SqlString `bun:"message_type,type:varchar(50),notnull," json:"message_type"` // text
|
||||||
|
Timestamp resolvespec_common.SqlTime `bun:"timestamp,type:timestamp,notnull," json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the table name for ModelPublicMessageCache
|
||||||
|
func (m ModelPublicMessageCache) TableName() string {
|
||||||
|
return "message_cache"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableNameOnly returns the table name without schema for ModelPublicMessageCache
|
||||||
|
func (m ModelPublicMessageCache) TableNameOnly() string {
|
||||||
|
return "message_cache"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaName returns the schema name for ModelPublicMessageCache
|
||||||
|
func (m ModelPublicMessageCache) SchemaName() string {
|
||||||
|
return "public"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the primary key value
|
||||||
|
func (m ModelPublicMessageCache) GetID() int64 {
|
||||||
|
return m.ID.Int64()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDStr returns the primary key as a string
|
||||||
|
func (m ModelPublicMessageCache) GetIDStr() string {
|
||||||
|
return fmt.Sprintf("%d", m.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetID sets the primary key value
|
||||||
|
func (m ModelPublicMessageCache) SetID(newid int64) {
|
||||||
|
m.UpdateID(newid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateID updates the primary key value
|
||||||
|
func (m *ModelPublicMessageCache) UpdateID(newid int64) {
|
||||||
|
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDName returns the name of the primary key column
|
||||||
|
func (m ModelPublicMessageCache) GetIDName() string {
|
||||||
|
return "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefix returns the table prefix
|
||||||
|
func (m ModelPublicMessageCache) GetPrefix() string {
|
||||||
|
return "MCE"
|
||||||
|
}
|
||||||
66
pkg/models/sql_public_sessions.go
Normal file
66
pkg/models/sql_public_sessions.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// Code generated by relspecgo. DO NOT EDIT.
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelPublicSession struct {
|
||||||
|
bun.BaseModel `bun:"table:sessions,alias:sessions"`
|
||||||
|
ID resolvespec_common.SqlString `bun:"id,type:varchar(36),pk," json:"id"` // UUID
|
||||||
|
CreatedAt resolvespec_common.SqlTime `bun:"created_at,type:timestamp,default:now(),notnull," json:"created_at"`
|
||||||
|
ExpiresAt resolvespec_common.SqlTime `bun:"expires_at,type:timestamp,notnull," json:"expires_at"`
|
||||||
|
IpAddress resolvespec_common.SqlString `bun:"ip_address,type:varchar(50),nullzero," json:"ip_address"`
|
||||||
|
Token resolvespec_common.SqlString `bun:"token,type:varchar(255),notnull," json:"token"` // Session token hash
|
||||||
|
UpdatedAt resolvespec_common.SqlTime `bun:"updated_at,type:timestamp,default:now(),notnull," json:"updated_at"`
|
||||||
|
UserAgent resolvespec_common.SqlString `bun:"user_agent,type:text,nullzero," json:"user_agent"`
|
||||||
|
UserID resolvespec_common.SqlString `bun:"user_id,type:varchar(36),notnull," json:"user_id"`
|
||||||
|
RelUserID *ModelPublicUser `bun:"rel:has-one,join:user_id=id" json:"reluserid,omitempty"` // Has one ModelPublicUser
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the table name for ModelPublicSession
|
||||||
|
func (m ModelPublicSession) TableName() string {
|
||||||
|
return "sessions"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableNameOnly returns the table name without schema for ModelPublicSession
|
||||||
|
func (m ModelPublicSession) TableNameOnly() string {
|
||||||
|
return "sessions"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaName returns the schema name for ModelPublicSession
|
||||||
|
func (m ModelPublicSession) SchemaName() string {
|
||||||
|
return "public"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the primary key value
|
||||||
|
func (m ModelPublicSession) GetID() int64 {
|
||||||
|
return m.ID.Int64()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDStr returns the primary key as a string
|
||||||
|
func (m ModelPublicSession) GetIDStr() string {
|
||||||
|
return fmt.Sprintf("%d", m.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetID sets the primary key value
|
||||||
|
func (m ModelPublicSession) SetID(newid int64) {
|
||||||
|
m.UpdateID(newid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateID updates the primary key value
|
||||||
|
func (m *ModelPublicSession) UpdateID(newid int64) {
|
||||||
|
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDName returns the name of the primary key column
|
||||||
|
func (m ModelPublicSession) GetIDName() string {
|
||||||
|
return "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefix returns the table prefix
|
||||||
|
func (m ModelPublicSession) GetPrefix() string {
|
||||||
|
return "SES"
|
||||||
|
}
|
||||||
72
pkg/models/sql_public_users.go
Normal file
72
pkg/models/sql_public_users.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// Code generated by relspecgo. DO NOT EDIT.
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelPublicUser struct {
|
||||||
|
bun.BaseModel `bun:"table:users,alias:users"`
|
||||||
|
ID resolvespec_common.SqlString `bun:"id,type:varchar(36),pk," json:"id"` // UUID
|
||||||
|
Active bool `bun:"active,type:boolean,default:true,notnull," json:"active"`
|
||||||
|
CreatedAt resolvespec_common.SqlTime `bun:"created_at,type:timestamp,default:now(),notnull," json:"created_at"`
|
||||||
|
DeletedAt resolvespec_common.SqlTime `bun:"deleted_at,type:timestamp,nullzero," json:"deleted_at"` // Soft delete
|
||||||
|
Email resolvespec_common.SqlString `bun:"email,type:varchar(255),notnull," json:"email"`
|
||||||
|
FullName resolvespec_common.SqlString `bun:"full_name,type:varchar(255),nullzero," json:"full_name"`
|
||||||
|
Password resolvespec_common.SqlString `bun:"password,type:varchar(255),notnull," json:"password"` // Bcrypt hashed password
|
||||||
|
Role resolvespec_common.SqlString `bun:"role,type:varchar(50),default:user,notnull," json:"role"` // admin
|
||||||
|
UpdatedAt resolvespec_common.SqlTime `bun:"updated_at,type:timestamp,default:now(),notnull," json:"updated_at"`
|
||||||
|
Username resolvespec_common.SqlString `bun:"username,type:varchar(255),notnull," json:"username"`
|
||||||
|
RelUserIDPublicAPIKeys []*ModelPublicAPIKey `bun:"rel:has-many,join:id=user_id" json:"reluseridpublicapikeys,omitempty"` // Has many ModelPublicAPIKey
|
||||||
|
RelUserIDPublicHooks []*ModelPublicHook `bun:"rel:has-many,join:id=user_id" json:"reluseridpublichooks,omitempty"` // Has many ModelPublicHook
|
||||||
|
RelUserIDPublicWhatsappAccounts []*ModelPublicWhatsappAccount `bun:"rel:has-many,join:id=user_id" json:"reluseridpublicwhatsappaccounts,omitempty"` // Has many ModelPublicWhatsappAccount
|
||||||
|
RelUserIDPublicEventLogs []*ModelPublicEventLog `bun:"rel:has-many,join:id=user_id" json:"reluseridpubliceventlogs,omitempty"` // Has many ModelPublicEventLog
|
||||||
|
RelUserIDPublicSessions []*ModelPublicSession `bun:"rel:has-many,join:id=user_id" json:"reluseridpublicsessions,omitempty"` // Has many ModelPublicSession
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the table name for ModelPublicUser
|
||||||
|
func (m ModelPublicUser) TableName() string {
|
||||||
|
return "users"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableNameOnly returns the table name without schema for ModelPublicUser
|
||||||
|
func (m ModelPublicUser) TableNameOnly() string {
|
||||||
|
return "users"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaName returns the schema name for ModelPublicUser
|
||||||
|
func (m ModelPublicUser) SchemaName() string {
|
||||||
|
return "public"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the primary key value
|
||||||
|
func (m ModelPublicUser) GetID() int64 {
|
||||||
|
return m.ID.Int64()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDStr returns the primary key as a string
|
||||||
|
func (m ModelPublicUser) GetIDStr() string {
|
||||||
|
return fmt.Sprintf("%d", m.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetID sets the primary key value
|
||||||
|
func (m ModelPublicUser) SetID(newid int64) {
|
||||||
|
m.UpdateID(newid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateID updates the primary key value
|
||||||
|
func (m *ModelPublicUser) UpdateID(newid int64) {
|
||||||
|
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDName returns the name of the primary key column
|
||||||
|
func (m ModelPublicUser) GetIDName() string {
|
||||||
|
return "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefix returns the table prefix
|
||||||
|
func (m ModelPublicUser) GetPrefix() string {
|
||||||
|
return "USE"
|
||||||
|
}
|
||||||
71
pkg/models/sql_public_whatsapp_accounts.go
Normal file
71
pkg/models/sql_public_whatsapp_accounts.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// Code generated by relspecgo. DO NOT EDIT.
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelPublicWhatsappAccount struct {
|
||||||
|
bun.BaseModel `bun:"table:whatsapp_accounts,alias:whatsapp_accounts"`
|
||||||
|
ID resolvespec_common.SqlString `bun:"id,type:varchar(36),pk," json:"id"` // UUID
|
||||||
|
AccountType resolvespec_common.SqlString `bun:"account_type,type:varchar(50),notnull," json:"account_type"` // whatsmeow or business-api
|
||||||
|
Active bool `bun:"active,type:boolean,default:true,notnull," json:"active"`
|
||||||
|
Config resolvespec_common.SqlString `bun:"config,type:text,nullzero," json:"config"` // JSON encoded additional config
|
||||||
|
CreatedAt resolvespec_common.SqlTime `bun:"created_at,type:timestamp,default:now(),notnull," json:"created_at"`
|
||||||
|
DeletedAt resolvespec_common.SqlTime `bun:"deleted_at,type:timestamp,nullzero," json:"deleted_at"`
|
||||||
|
DisplayName resolvespec_common.SqlString `bun:"display_name,type:varchar(255),nullzero," json:"display_name"`
|
||||||
|
LastConnectedAt resolvespec_common.SqlTime `bun:"last_connected_at,type:timestamp,nullzero," json:"last_connected_at"`
|
||||||
|
PhoneNumber resolvespec_common.SqlString `bun:"phone_number,type:varchar(50),notnull," json:"phone_number"`
|
||||||
|
SessionPath resolvespec_common.SqlString `bun:"session_path,type:text,nullzero," json:"session_path"`
|
||||||
|
Status resolvespec_common.SqlString `bun:"status,type:varchar(50),default:disconnected,notnull," json:"status"` // connected
|
||||||
|
UpdatedAt resolvespec_common.SqlTime `bun:"updated_at,type:timestamp,default:now(),notnull," json:"updated_at"`
|
||||||
|
UserID resolvespec_common.SqlString `bun:"user_id,type:varchar(36),notnull," json:"user_id"`
|
||||||
|
RelUserID *ModelPublicUser `bun:"rel:has-one,join:user_id=id" json:"reluserid,omitempty"` // Has one ModelPublicUser
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the table name for ModelPublicWhatsappAccount
|
||||||
|
func (m ModelPublicWhatsappAccount) TableName() string {
|
||||||
|
return "whatsapp_accounts"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableNameOnly returns the table name without schema for ModelPublicWhatsappAccount
|
||||||
|
func (m ModelPublicWhatsappAccount) TableNameOnly() string {
|
||||||
|
return "whatsapp_accounts"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaName returns the schema name for ModelPublicWhatsappAccount
|
||||||
|
func (m ModelPublicWhatsappAccount) SchemaName() string {
|
||||||
|
return "public"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the primary key value
|
||||||
|
func (m ModelPublicWhatsappAccount) GetID() int64 {
|
||||||
|
return m.ID.Int64()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDStr returns the primary key as a string
|
||||||
|
func (m ModelPublicWhatsappAccount) GetIDStr() string {
|
||||||
|
return fmt.Sprintf("%d", m.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetID sets the primary key value
|
||||||
|
func (m ModelPublicWhatsappAccount) SetID(newid int64) {
|
||||||
|
m.UpdateID(newid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateID updates the primary key value
|
||||||
|
func (m *ModelPublicWhatsappAccount) UpdateID(newid int64) {
|
||||||
|
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDName returns the name of the primary key column
|
||||||
|
func (m ModelPublicWhatsappAccount) GetIDName() string {
|
||||||
|
return "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefix returns the table prefix
|
||||||
|
func (m ModelPublicWhatsappAccount) GetPrefix() string {
|
||||||
|
return "WAH"
|
||||||
|
}
|
||||||
246
pkg/storage/db.go
Normal file
246
pkg/storage/db.go
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/config"
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/models"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
"github.com/uptrace/bun/dialect/pgdialect"
|
||||||
|
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||||
|
"github.com/uptrace/bun/driver/pgdriver"
|
||||||
|
"github.com/uptrace/bun/driver/sqliteshim"
|
||||||
|
"github.com/uptrace/bun/extra/bundebug"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DB is the global database instance
|
||||||
|
var DB *bun.DB
|
||||||
|
var dbType string // Store the database type for later use
|
||||||
|
|
||||||
|
// Initialize sets up the database connection based on configuration
|
||||||
|
func Initialize(cfg *config.DatabaseConfig) error {
|
||||||
|
var sqldb *sql.DB
|
||||||
|
var err error
|
||||||
|
|
||||||
|
dbType = cfg.Type
|
||||||
|
switch cfg.Type {
|
||||||
|
case "postgres", "postgresql":
|
||||||
|
dsn := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable",
|
||||||
|
cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
|
||||||
|
sqldb = sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
|
||||||
|
DB = bun.NewDB(sqldb, pgdialect.New())
|
||||||
|
|
||||||
|
case "sqlite":
|
||||||
|
sqldb, err = sql.Open(sqliteshim.ShimName, cfg.SQLitePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open sqlite database: %w", err)
|
||||||
|
}
|
||||||
|
DB = bun.NewDB(sqldb, sqlitedialect.New())
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported database type: %s", cfg.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add query hook for debugging (optional, can be removed in production)
|
||||||
|
DB.AddQueryHook(bundebug.NewQueryHook(
|
||||||
|
bundebug.WithVerbose(true),
|
||||||
|
bundebug.FromEnv("BUNDEBUG"),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Set connection pool settings
|
||||||
|
sqldb.SetMaxIdleConns(10)
|
||||||
|
sqldb.SetMaxOpenConns(100)
|
||||||
|
sqldb.SetConnMaxLifetime(time.Hour)
|
||||||
|
|
||||||
|
// Test the connection
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := sqldb.PingContext(ctx); err != nil {
|
||||||
|
return fmt.Errorf("failed to ping database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTables creates database tables based on BUN models
|
||||||
|
func CreateTables(ctx context.Context) error {
|
||||||
|
if DB == nil {
|
||||||
|
return fmt.Errorf("database not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// For SQLite, use raw SQL with compatible syntax
|
||||||
|
if dbType == "sqlite" {
|
||||||
|
return createTablesSQLite(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For PostgreSQL, use BUN's auto-generation
|
||||||
|
models := []interface{}{
|
||||||
|
(*models.ModelPublicUser)(nil),
|
||||||
|
(*models.ModelPublicAPIKey)(nil),
|
||||||
|
(*models.ModelPublicHook)(nil),
|
||||||
|
(*models.ModelPublicWhatsappAccount)(nil),
|
||||||
|
(*models.ModelPublicEventLog)(nil),
|
||||||
|
(*models.ModelPublicSession)(nil),
|
||||||
|
(*models.ModelPublicMessageCache)(nil),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, model := range models {
|
||||||
|
_, err := DB.NewCreateTable().Model(model).IfNotExists().Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create table: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTablesSQLite creates tables using SQLite-compatible SQL
|
||||||
|
func createTablesSQLite(ctx context.Context) error {
|
||||||
|
tables := []string{
|
||||||
|
// Users table
|
||||||
|
`CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
|
username VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
full_name VARCHAR(255),
|
||||||
|
role VARCHAR(50) NOT NULL DEFAULT 'user',
|
||||||
|
active BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP
|
||||||
|
)`,
|
||||||
|
|
||||||
|
// API Keys table
|
||||||
|
`CREATE TABLE IF NOT EXISTS api_keys (
|
||||||
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
|
user_id VARCHAR(36) NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
key VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
key_prefix VARCHAR(20),
|
||||||
|
permissions TEXT,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
last_used_at TIMESTAMP,
|
||||||
|
expires_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
)`,
|
||||||
|
|
||||||
|
// Hooks table
|
||||||
|
`CREATE TABLE IF NOT EXISTS hooks (
|
||||||
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
|
user_id VARCHAR(36) NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
method VARCHAR(10) NOT NULL DEFAULT 'POST',
|
||||||
|
headers TEXT,
|
||||||
|
events TEXT,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
retry_count INTEGER NOT NULL DEFAULT 3,
|
||||||
|
timeout_seconds INTEGER NOT NULL DEFAULT 30,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
)`,
|
||||||
|
|
||||||
|
// WhatsApp Accounts table
|
||||||
|
`CREATE TABLE IF NOT EXISTS whatsapp_accounts (
|
||||||
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
|
user_id VARCHAR(36) NOT NULL,
|
||||||
|
phone_number VARCHAR(20) NOT NULL UNIQUE,
|
||||||
|
account_type VARCHAR(50) NOT NULL DEFAULT 'whatsmeow',
|
||||||
|
business_api_config TEXT,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
connected BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
last_connected_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
)`,
|
||||||
|
|
||||||
|
// Event Logs table
|
||||||
|
`CREATE TABLE IF NOT EXISTS event_logs (
|
||||||
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
|
user_id VARCHAR(36),
|
||||||
|
account_id VARCHAR(36),
|
||||||
|
event_type VARCHAR(100) NOT NULL,
|
||||||
|
event_data TEXT,
|
||||||
|
from_number VARCHAR(20),
|
||||||
|
to_number VARCHAR(20),
|
||||||
|
message_id VARCHAR(255),
|
||||||
|
status VARCHAR(50),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (account_id) REFERENCES whatsapp_accounts(id) ON DELETE SET NULL
|
||||||
|
)`,
|
||||||
|
|
||||||
|
// Sessions table
|
||||||
|
`CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
|
user_id VARCHAR(36) NOT NULL,
|
||||||
|
token VARCHAR(500) NOT NULL UNIQUE,
|
||||||
|
expires_at TIMESTAMP NOT NULL,
|
||||||
|
ip_address VARCHAR(45),
|
||||||
|
user_agent TEXT,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
)`,
|
||||||
|
|
||||||
|
// Message Cache table
|
||||||
|
`CREATE TABLE IF NOT EXISTS message_cache (
|
||||||
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
|
account_id VARCHAR(36),
|
||||||
|
event_type VARCHAR(100) NOT NULL,
|
||||||
|
event_data TEXT NOT NULL,
|
||||||
|
message_id VARCHAR(255),
|
||||||
|
from_number VARCHAR(20),
|
||||||
|
to_number VARCHAR(20),
|
||||||
|
processed BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
processed_at TIMESTAMP,
|
||||||
|
FOREIGN KEY (account_id) REFERENCES whatsapp_accounts(id) ON DELETE SET NULL
|
||||||
|
)`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sql := range tables {
|
||||||
|
if _, err := DB.ExecContext(ctx, sql); err != nil {
|
||||||
|
return fmt.Errorf("failed to create table: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the database connection
|
||||||
|
func Close() error {
|
||||||
|
if DB == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDB returns the database instance
|
||||||
|
func GetDB() *bun.DB {
|
||||||
|
return DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheck checks if the database connection is healthy
|
||||||
|
func HealthCheck() error {
|
||||||
|
if DB == nil {
|
||||||
|
return fmt.Errorf("database not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
return DB.PingContext(ctx)
|
||||||
|
}
|
||||||
299
pkg/storage/repository.go
Normal file
299
pkg/storage/repository.go
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/models"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Repository provides common CRUD operations
|
||||||
|
type Repository[T any] struct {
|
||||||
|
db *bun.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRepository creates a new repository instance
|
||||||
|
func NewRepository[T any](db *bun.DB) *Repository[T] {
|
||||||
|
return &Repository[T]{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create inserts a new record
|
||||||
|
func (r *Repository[T]) Create(ctx context.Context, entity *T) error {
|
||||||
|
_, err := r.db.NewInsert().Model(entity).Exec(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID retrieves a record by ID
|
||||||
|
func (r *Repository[T]) GetByID(ctx context.Context, id string) (*T, error) {
|
||||||
|
var entity T
|
||||||
|
err := r.db.NewSelect().Model(&entity).Where("id = ?", id).Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &entity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates an existing record
|
||||||
|
func (r *Repository[T]) Update(ctx context.Context, entity *T) error {
|
||||||
|
_, err := r.db.NewUpdate().Model(entity).WherePK().Exec(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete soft deletes a record by ID (if model has DeletedAt field)
|
||||||
|
func (r *Repository[T]) Delete(ctx context.Context, id string) error {
|
||||||
|
var entity T
|
||||||
|
_, err := r.db.NewDelete().Model(&entity).Where("id = ?", id).Exec(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// List retrieves all records with optional filtering
|
||||||
|
func (r *Repository[T]) List(ctx context.Context, filter map[string]interface{}) ([]T, error) {
|
||||||
|
var entities []T
|
||||||
|
query := r.db.NewSelect().Model(&entities)
|
||||||
|
|
||||||
|
for key, value := range filter {
|
||||||
|
query = query.Where("? = ?", bun.Ident(key), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := query.Scan(ctx)
|
||||||
|
return entities, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count returns the total number of records matching the filter
|
||||||
|
func (r *Repository[T]) Count(ctx context.Context, filter map[string]interface{}) (int, error) {
|
||||||
|
query := r.db.NewSelect().Model((*T)(nil))
|
||||||
|
|
||||||
|
for key, value := range filter {
|
||||||
|
query = query.Where("? = ?", bun.Ident(key), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := query.Count(ctx)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserRepository provides user-specific operations
|
||||||
|
type UserRepository struct {
|
||||||
|
*Repository[models.ModelPublicUser]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserRepository creates a new user repository
|
||||||
|
func NewUserRepository(db *bun.DB) *UserRepository {
|
||||||
|
return &UserRepository{
|
||||||
|
Repository: NewRepository[models.ModelPublicUser](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByUsername retrieves a user by username
|
||||||
|
func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*models.ModelPublicUser, error) {
|
||||||
|
var user models.ModelPublicUser
|
||||||
|
err := r.db.NewSelect().Model(&user).Where("username = ?", username).Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByEmail retrieves a user by email
|
||||||
|
func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*models.ModelPublicUser, error) {
|
||||||
|
var user models.ModelPublicUser
|
||||||
|
err := r.db.NewSelect().Model(&user).Where("email = ?", email).Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIKeyRepository provides API key-specific operations
|
||||||
|
type APIKeyRepository struct {
|
||||||
|
*Repository[models.ModelPublicAPIKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAPIKeyRepository creates a new API key repository
|
||||||
|
func NewAPIKeyRepository(db *bun.DB) *APIKeyRepository {
|
||||||
|
return &APIKeyRepository{
|
||||||
|
Repository: NewRepository[models.ModelPublicAPIKey](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByKey retrieves an API key by its key value
|
||||||
|
func (r *APIKeyRepository) GetByKey(ctx context.Context, key string) (*models.ModelPublicAPIKey, error) {
|
||||||
|
var apiKey models.ModelPublicAPIKey
|
||||||
|
err := r.db.NewSelect().Model(&apiKey).
|
||||||
|
Where("key = ? AND active = ?", key, true).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &apiKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByUserID retrieves all API keys for a user
|
||||||
|
func (r *APIKeyRepository) GetByUserID(ctx context.Context, userID string) ([]models.ModelPublicAPIKey, error) {
|
||||||
|
var apiKeys []models.ModelPublicAPIKey
|
||||||
|
err := r.db.NewSelect().Model(&apiKeys).Where("user_id = ?", userID).Scan(ctx)
|
||||||
|
return apiKeys, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLastUsed updates the last used timestamp for an API key
|
||||||
|
func (r *APIKeyRepository) UpdateLastUsed(ctx context.Context, id string) error {
|
||||||
|
now := time.Now()
|
||||||
|
_, err := r.db.NewUpdate().Model((*models.ModelPublicAPIKey)(nil)).
|
||||||
|
Set("last_used_at = ?", now).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Exec(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// HookRepository provides hook-specific operations
|
||||||
|
type HookRepository struct {
|
||||||
|
*Repository[models.ModelPublicHook]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHookRepository creates a new hook repository
|
||||||
|
func NewHookRepository(db *bun.DB) *HookRepository {
|
||||||
|
return &HookRepository{
|
||||||
|
Repository: NewRepository[models.ModelPublicHook](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByUserID retrieves all hooks for a user
|
||||||
|
func (r *HookRepository) GetByUserID(ctx context.Context, userID string) ([]models.ModelPublicHook, error) {
|
||||||
|
var hooks []models.ModelPublicHook
|
||||||
|
err := r.db.NewSelect().Model(&hooks).Where("user_id = ?", userID).Scan(ctx)
|
||||||
|
return hooks, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveHooks retrieves all active hooks
|
||||||
|
func (r *HookRepository) GetActiveHooks(ctx context.Context) ([]models.ModelPublicHook, error) {
|
||||||
|
var hooks []models.ModelPublicHook
|
||||||
|
err := r.db.NewSelect().Model(&hooks).Where("active = ?", true).Scan(ctx)
|
||||||
|
return hooks, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WhatsAppAccountRepository provides WhatsApp account-specific operations
|
||||||
|
type WhatsAppAccountRepository struct {
|
||||||
|
*Repository[models.ModelPublicWhatsappAccount]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWhatsAppAccountRepository creates a new WhatsApp account repository
|
||||||
|
func NewWhatsAppAccountRepository(db *bun.DB) *WhatsAppAccountRepository {
|
||||||
|
return &WhatsAppAccountRepository{
|
||||||
|
Repository: NewRepository[models.ModelPublicWhatsappAccount](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByUserID retrieves all WhatsApp accounts for a user
|
||||||
|
func (r *WhatsAppAccountRepository) GetByUserID(ctx context.Context, userID string) ([]models.ModelPublicWhatsappAccount, error) {
|
||||||
|
var accounts []models.ModelPublicWhatsappAccount
|
||||||
|
err := r.db.NewSelect().Model(&accounts).Where("user_id = ?", userID).Scan(ctx)
|
||||||
|
return accounts, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByPhoneNumber retrieves an account by phone number
|
||||||
|
func (r *WhatsAppAccountRepository) GetByPhoneNumber(ctx context.Context, phoneNumber string) (*models.ModelPublicWhatsappAccount, error) {
|
||||||
|
var account models.ModelPublicWhatsappAccount
|
||||||
|
err := r.db.NewSelect().Model(&account).Where("phone_number = ?", phoneNumber).Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStatus updates the status of a WhatsApp account
|
||||||
|
func (r *WhatsAppAccountRepository) UpdateStatus(ctx context.Context, id string, status string) error {
|
||||||
|
query := r.db.NewUpdate().Model((*models.ModelPublicWhatsappAccount)(nil)).
|
||||||
|
Set("status = ?", status).
|
||||||
|
Where("id = ?", id)
|
||||||
|
|
||||||
|
if status == "connected" {
|
||||||
|
now := time.Now()
|
||||||
|
query = query.Set("last_connected_at = ?", now)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := query.Exec(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionRepository provides session-specific operations
|
||||||
|
type SessionRepository struct {
|
||||||
|
*Repository[models.ModelPublicSession]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSessionRepository creates a new session repository
|
||||||
|
func NewSessionRepository(db *bun.DB) *SessionRepository {
|
||||||
|
return &SessionRepository{
|
||||||
|
Repository: NewRepository[models.ModelPublicSession](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByToken retrieves a session by token
|
||||||
|
func (r *SessionRepository) GetByToken(ctx context.Context, token string) (*models.ModelPublicSession, error) {
|
||||||
|
var session models.ModelPublicSession
|
||||||
|
err := r.db.NewSelect().Model(&session).
|
||||||
|
Where("token = ? AND expires_at > ?", token, time.Now()).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteExpired removes all expired sessions
|
||||||
|
func (r *SessionRepository) DeleteExpired(ctx context.Context) error {
|
||||||
|
_, err := r.db.NewDelete().Model((*models.ModelPublicSession)(nil)).
|
||||||
|
Where("expires_at <= ?", time.Now()).
|
||||||
|
Exec(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByUserID removes all sessions for a user
|
||||||
|
func (r *SessionRepository) DeleteByUserID(ctx context.Context, userID string) error {
|
||||||
|
_, err := r.db.NewDelete().Model((*models.ModelPublicSession)(nil)).
|
||||||
|
Where("user_id = ?", userID).
|
||||||
|
Exec(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventLogRepository provides event log-specific operations
|
||||||
|
type EventLogRepository struct {
|
||||||
|
*Repository[models.ModelPublicEventLog]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEventLogRepository creates a new event log repository
|
||||||
|
func NewEventLogRepository(db *bun.DB) *EventLogRepository {
|
||||||
|
return &EventLogRepository{
|
||||||
|
Repository: NewRepository[models.ModelPublicEventLog](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByUserID retrieves event logs for a user
|
||||||
|
func (r *EventLogRepository) GetByUserID(ctx context.Context, userID string, limit int) ([]models.ModelPublicEventLog, error) {
|
||||||
|
var logs []models.ModelPublicEventLog
|
||||||
|
err := r.db.NewSelect().Model(&logs).
|
||||||
|
Where("user_id = ?", userID).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Limit(limit).
|
||||||
|
Scan(ctx)
|
||||||
|
return logs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByEntityID retrieves event logs for a specific entity
|
||||||
|
func (r *EventLogRepository) GetByEntityID(ctx context.Context, entityType, entityID string, limit int) ([]models.ModelPublicEventLog, error) {
|
||||||
|
var logs []models.ModelPublicEventLog
|
||||||
|
err := r.db.NewSelect().Model(&logs).
|
||||||
|
Where("entity_type = ? AND entity_id = ?", entityType, entityID).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Limit(limit).
|
||||||
|
Scan(ctx)
|
||||||
|
return logs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteOlderThan removes event logs older than the specified duration
|
||||||
|
func (r *EventLogRepository) DeleteOlderThan(ctx context.Context, duration time.Duration) error {
|
||||||
|
cutoff := time.Now().Add(-duration)
|
||||||
|
_, err := r.db.NewDelete().Model((*models.ModelPublicEventLog)(nil)).
|
||||||
|
Where("created_at < ?", cutoff).
|
||||||
|
Exec(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
55
pkg/storage/seed.go
Normal file
55
pkg/storage/seed.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/models"
|
||||||
|
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SeedData creates initial data for the application
|
||||||
|
func SeedData(ctx context.Context) error {
|
||||||
|
if DB == nil {
|
||||||
|
return fmt.Errorf("database not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if admin user already exists
|
||||||
|
userRepo := NewUserRepository(DB)
|
||||||
|
_, err := userRepo.GetByUsername(ctx, "admin")
|
||||||
|
if err == nil {
|
||||||
|
// Admin user already exists
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create default admin user
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to hash password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
adminUser := &models.ModelPublicUser{
|
||||||
|
ID: resolvespec_common.NewSqlString(uuid.New().String()),
|
||||||
|
Username: resolvespec_common.NewSqlString("admin"),
|
||||||
|
Email: resolvespec_common.NewSqlString("admin@whatshooked.local"),
|
||||||
|
Password: resolvespec_common.NewSqlString(string(hashedPassword)),
|
||||||
|
FullName: resolvespec_common.NewSqlString("System Administrator"),
|
||||||
|
Role: resolvespec_common.NewSqlString("admin"),
|
||||||
|
Active: true,
|
||||||
|
CreatedAt: resolvespec_common.NewSqlTime(now),
|
||||||
|
UpdatedAt: resolvespec_common.NewSqlTime(now),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := userRepo.Create(ctx, adminUser); err != nil {
|
||||||
|
return fmt.Errorf("failed to create admin user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("✓ Created default admin user (username: admin, password: admin123)")
|
||||||
|
fmt.Println("⚠ Please change the default password after first login!")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/api"
|
||||||
"git.warky.dev/wdevs/whatshooked/pkg/cache"
|
"git.warky.dev/wdevs/whatshooked/pkg/cache"
|
||||||
"git.warky.dev/wdevs/whatshooked/pkg/config"
|
"git.warky.dev/wdevs/whatshooked/pkg/config"
|
||||||
"git.warky.dev/wdevs/whatshooked/pkg/eventlogger"
|
"git.warky.dev/wdevs/whatshooked/pkg/eventlogger"
|
||||||
@@ -11,7 +12,10 @@ import (
|
|||||||
"git.warky.dev/wdevs/whatshooked/pkg/handlers"
|
"git.warky.dev/wdevs/whatshooked/pkg/handlers"
|
||||||
"git.warky.dev/wdevs/whatshooked/pkg/hooks"
|
"git.warky.dev/wdevs/whatshooked/pkg/hooks"
|
||||||
"git.warky.dev/wdevs/whatshooked/pkg/logging"
|
"git.warky.dev/wdevs/whatshooked/pkg/logging"
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/storage"
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/utils"
|
||||||
"git.warky.dev/wdevs/whatshooked/pkg/whatsapp"
|
"git.warky.dev/wdevs/whatshooked/pkg/whatsapp"
|
||||||
|
"go.mau.fi/whatsmeow/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WhatsHooked is the main library instance
|
// WhatsHooked is the main library instance
|
||||||
@@ -24,7 +28,7 @@ type WhatsHooked struct {
|
|||||||
eventLogger *eventlogger.Logger
|
eventLogger *eventlogger.Logger
|
||||||
messageCache *cache.MessageCache
|
messageCache *cache.MessageCache
|
||||||
handlers *handlers.Handlers
|
handlers *handlers.Handlers
|
||||||
server *Server // Optional built-in server
|
apiServer *api.Server // ResolveSpec unified server
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFromFile creates a WhatsHooked instance from a config file
|
// NewFromFile creates a WhatsHooked instance from a config file
|
||||||
@@ -179,16 +183,116 @@ func (wh *WhatsHooked) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartServer starts the built-in HTTP server (convenience method)
|
// StartServer starts the ResolveSpec HTTP server
|
||||||
func (wh *WhatsHooked) StartServer() error {
|
func (wh *WhatsHooked) StartServer(ctx context.Context) error {
|
||||||
wh.server = NewServer(wh)
|
return wh.StartAPIServer(ctx)
|
||||||
return wh.server.Start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopServer stops the built-in HTTP server
|
// StopServer stops the ResolveSpec HTTP server
|
||||||
func (wh *WhatsHooked) StopServer(ctx context.Context) error {
|
func (wh *WhatsHooked) StopServer(ctx context.Context) error {
|
||||||
if wh.server != nil {
|
return wh.StopAPIServer(ctx)
|
||||||
return wh.server.Stop(ctx)
|
}
|
||||||
|
|
||||||
|
// StartAPIServer starts the unified ResolveSpec server
|
||||||
|
func (wh *WhatsHooked) StartAPIServer(ctx context.Context) error {
|
||||||
|
// Subscribe to hook success events for two-way communication
|
||||||
|
wh.eventBus.Subscribe(events.EventHookSuccess, wh.handleHookResponse)
|
||||||
|
|
||||||
|
// Initialize database
|
||||||
|
logging.Info("Initializing database")
|
||||||
|
if err := storage.Initialize(&wh.config.Database); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
db := storage.GetDB()
|
||||||
|
|
||||||
|
// Create tables
|
||||||
|
logging.Info("Creating database tables")
|
||||||
|
if err := storage.CreateTables(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed initial data (creates admin user if not exists)
|
||||||
|
logging.Info("Seeding initial data")
|
||||||
|
if err := storage.SeedData(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create unified server
|
||||||
|
logging.Info("Creating ResolveSpec server", "host", wh.config.Server.Host, "port", wh.config.Server.Port)
|
||||||
|
apiServer, err := api.NewServer(wh.config, db, wh)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
wh.apiServer = apiServer
|
||||||
|
|
||||||
|
// Start the server in a goroutine (non-blocking)
|
||||||
|
go func() {
|
||||||
|
if err := wh.apiServer.Start(); err != nil {
|
||||||
|
logging.Error("ResolveSpec server error", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
logging.Info("ResolveSpec server started successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopAPIServer stops the ResolveSpec server
|
||||||
|
func (wh *WhatsHooked) StopAPIServer(ctx context.Context) error {
|
||||||
|
if wh.apiServer != nil {
|
||||||
|
logging.Info("Stopping ResolveSpec server")
|
||||||
|
// The server manager handles graceful shutdown internally
|
||||||
|
// We just need to close the database
|
||||||
|
return storage.Close()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleHookResponse processes hook success events for two-way communication
|
||||||
|
func (wh *WhatsHooked) handleHookResponse(event events.Event) {
|
||||||
|
// Use event context for sending message
|
||||||
|
ctx := event.Context
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract response from event data
|
||||||
|
responseData, ok := event.Data["response"]
|
||||||
|
if !ok || responseData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to cast to HookResponse
|
||||||
|
resp, ok := responseData.(hooks.HookResponse)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.SendMessage {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine which account to use - default to first available if not specified
|
||||||
|
targetAccountID := resp.AccountID
|
||||||
|
if targetAccountID == "" && len(wh.config.WhatsApp) > 0 {
|
||||||
|
targetAccountID = wh.config.WhatsApp[0].ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format phone number to JID format
|
||||||
|
formattedJID := utils.FormatPhoneToJID(resp.To, wh.config.Server.DefaultCountryCode)
|
||||||
|
|
||||||
|
// Parse JID
|
||||||
|
jid, err := types.ParseJID(formattedJID)
|
||||||
|
if err != nil {
|
||||||
|
logging.Error("Invalid JID in hook response", "jid", formattedJID, "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send message with context
|
||||||
|
if err := wh.whatsappMgr.SendTextMessage(ctx, targetAccountID, jid, resp.Text); err != nil {
|
||||||
|
logging.Error("Failed to send message from hook response", "error", err)
|
||||||
|
} else {
|
||||||
|
logging.Info("Message sent from hook response", "account_id", targetAccountID, "to", resp.To)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
11
sql/postgres/001_init_schema.down.sql
Normal file
11
sql/postgres/001_init_schema.down.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- Rollback migration for initial schema
|
||||||
|
-- This drops all tables created in 001_init_schema.up.sql
|
||||||
|
|
||||||
|
-- Drop tables in reverse order (to handle foreign key constraints)
|
||||||
|
DROP TABLE IF EXISTS public.message_cache CASCADE;
|
||||||
|
DROP TABLE IF EXISTS public.sessions CASCADE;
|
||||||
|
DROP TABLE IF EXISTS public.event_logs CASCADE;
|
||||||
|
DROP TABLE IF EXISTS public.whatsapp_accounts CASCADE;
|
||||||
|
DROP TABLE IF EXISTS public.hooks CASCADE;
|
||||||
|
DROP TABLE IF EXISTS public.api_keys CASCADE;
|
||||||
|
DROP TABLE IF EXISTS public.users CASCADE;
|
||||||
1556
sql/postgres/001_init_schema.up.sql
Normal file
1556
sql/postgres/001_init_schema.up.sql
Normal file
File diff suppressed because it is too large
Load Diff
147
sql/schema.dbml
Normal file
147
sql/schema.dbml
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
// WhatsHooked Database Schema
|
||||||
|
// This file defines the database schema for WhatsHooked Phase 2
|
||||||
|
|
||||||
|
Table users {
|
||||||
|
id varchar(36) [primary key, note: 'UUID']
|
||||||
|
username varchar(255) [unique, not null]
|
||||||
|
email varchar(255) [unique, not null]
|
||||||
|
password varchar(255) [not null, note: 'Bcrypt hashed password']
|
||||||
|
full_name varchar(255)
|
||||||
|
role varchar(50) [not null, default: 'user', note: 'admin, user, viewer']
|
||||||
|
active boolean [not null, default: true]
|
||||||
|
created_at timestamp [not null, default: `now()`]
|
||||||
|
updated_at timestamp [not null, default: `now()`]
|
||||||
|
deleted_at timestamp [null, note: 'Soft delete']
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(deleted_at) [name: 'idx_users_deleted_at']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Table api_keys {
|
||||||
|
id varchar(36) [primary key, note: 'UUID']
|
||||||
|
user_id varchar(36) [not null, ref: > users.id]
|
||||||
|
name varchar(255) [not null, note: 'Friendly name for the API key']
|
||||||
|
key varchar(255) [unique, not null, note: 'Hashed API key']
|
||||||
|
key_prefix varchar(20) [note: 'First few characters for display']
|
||||||
|
permissions text [note: 'JSON array of permissions']
|
||||||
|
last_used_at timestamp [null]
|
||||||
|
expires_at timestamp [null]
|
||||||
|
active boolean [not null, default: true]
|
||||||
|
created_at timestamp [not null, default: `now()`]
|
||||||
|
updated_at timestamp [not null, default: `now()`]
|
||||||
|
deleted_at timestamp [null]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(user_id) [name: 'idx_api_keys_user_id']
|
||||||
|
(deleted_at) [name: 'idx_api_keys_deleted_at']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Table hooks {
|
||||||
|
id varchar(36) [primary key, note: 'UUID']
|
||||||
|
user_id varchar(36) [not null, ref: > users.id]
|
||||||
|
name varchar(255) [not null]
|
||||||
|
url text [not null]
|
||||||
|
method varchar(10) [not null, default: 'POST', note: 'HTTP method']
|
||||||
|
headers text [note: 'JSON encoded headers']
|
||||||
|
events text [note: 'JSON array of event types']
|
||||||
|
active boolean [not null, default: true]
|
||||||
|
description text
|
||||||
|
secret varchar(255) [note: 'HMAC signature secret']
|
||||||
|
retry_count int [not null, default: 3]
|
||||||
|
timeout int [not null, default: 30, note: 'Timeout in seconds']
|
||||||
|
created_at timestamp [not null, default: `now()`]
|
||||||
|
updated_at timestamp [not null, default: `now()`]
|
||||||
|
deleted_at timestamp [null]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(user_id) [name: 'idx_hooks_user_id']
|
||||||
|
(deleted_at) [name: 'idx_hooks_deleted_at']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Table whatsapp_accounts {
|
||||||
|
id varchar(36) [primary key, note: 'UUID']
|
||||||
|
user_id varchar(36) [not null, ref: > users.id]
|
||||||
|
account_type varchar(50) [not null, note: 'whatsmeow or business-api']
|
||||||
|
phone_number varchar(50) [unique, not null]
|
||||||
|
display_name varchar(255)
|
||||||
|
session_path text
|
||||||
|
status varchar(50) [not null, default: 'disconnected', note: 'connected, disconnected, pairing']
|
||||||
|
last_connected_at timestamp [null]
|
||||||
|
active boolean [not null, default: true]
|
||||||
|
config text [note: 'JSON encoded additional config']
|
||||||
|
created_at timestamp [not null, default: `now()`]
|
||||||
|
updated_at timestamp [not null, default: `now()`]
|
||||||
|
deleted_at timestamp [null]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(user_id) [name: 'idx_whatsapp_accounts_user_id']
|
||||||
|
(deleted_at) [name: 'idx_whatsapp_accounts_deleted_at']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Table event_logs {
|
||||||
|
id varchar(36) [primary key, note: 'UUID']
|
||||||
|
user_id varchar(36) [ref: > users.id, note: 'Optional user reference']
|
||||||
|
event_type varchar(100) [not null]
|
||||||
|
entity_type varchar(100) [note: 'user, hook, account, etc.']
|
||||||
|
entity_id varchar(36)
|
||||||
|
action varchar(50) [note: 'create, update, delete, read']
|
||||||
|
data text [note: 'JSON encoded event data']
|
||||||
|
ip_address varchar(50)
|
||||||
|
user_agent text
|
||||||
|
success boolean [not null, default: true]
|
||||||
|
error text
|
||||||
|
created_at timestamp [not null, default: `now()`]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(user_id) [name: 'idx_event_logs_user_id']
|
||||||
|
(event_type) [name: 'idx_event_logs_event_type']
|
||||||
|
(entity_type) [name: 'idx_event_logs_entity_type']
|
||||||
|
(entity_id) [name: 'idx_event_logs_entity_id']
|
||||||
|
(created_at) [name: 'idx_event_logs_created_at']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Table sessions {
|
||||||
|
id varchar(36) [primary key, note: 'UUID']
|
||||||
|
user_id varchar(36) [not null, ref: > users.id]
|
||||||
|
token varchar(255) [unique, not null, note: 'Session token hash']
|
||||||
|
ip_address varchar(50)
|
||||||
|
user_agent text
|
||||||
|
expires_at timestamp [not null]
|
||||||
|
created_at timestamp [not null, default: `now()`]
|
||||||
|
updated_at timestamp [not null, default: `now()`]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(user_id) [name: 'idx_sessions_user_id']
|
||||||
|
(expires_at) [name: 'idx_sessions_expires_at']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Table message_cache {
|
||||||
|
id varchar(36) [primary key, note: 'UUID']
|
||||||
|
account_id varchar(36) [not null]
|
||||||
|
message_id varchar(255) [unique, not null]
|
||||||
|
chat_id varchar(255) [not null]
|
||||||
|
from_me boolean [not null]
|
||||||
|
timestamp timestamp [not null]
|
||||||
|
message_type varchar(50) [not null, note: 'text, image, video, etc.']
|
||||||
|
content text [not null, note: 'JSON encoded message content']
|
||||||
|
created_at timestamp [not null, default: `now()`]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(account_id) [name: 'idx_message_cache_account_id']
|
||||||
|
(chat_id) [name: 'idx_message_cache_chat_id']
|
||||||
|
(from_me) [name: 'idx_message_cache_from_me']
|
||||||
|
(timestamp) [name: 'idx_message_cache_timestamp']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference documentation
|
||||||
|
Ref: api_keys.user_id > users.id [delete: cascade]
|
||||||
|
Ref: hooks.user_id > users.id [delete: cascade]
|
||||||
|
Ref: whatsapp_accounts.user_id > users.id [delete: cascade]
|
||||||
|
Ref: sessions.user_id > users.id [delete: cascade]
|
||||||
557
tooldoc/BUN_ORM.md
Normal file
557
tooldoc/BUN_ORM.md
Normal file
@@ -0,0 +1,557 @@
|
|||||||
|
# BUN ORM Integration Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
BUN is a fast and lightweight SQL-first ORM for Go. For WhatsHooked Phase 2, we use BUN with PostgreSQL and SQLite, integrated with ResolveSpec for REST API generation.
|
||||||
|
|
||||||
|
Official Documentation: https://bun.uptrace.dev/
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/uptrace/bun
|
||||||
|
go get github.com/uptrace/bun/driver/pgdriver # PostgreSQL
|
||||||
|
go get github.com/uptrace/bun/driver/sqliteshim # SQLite
|
||||||
|
go get github.com/uptrace/bun/dialect/pgdialect
|
||||||
|
go get github.com/uptrace/bun/dialect/sqlitedialect
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Database Connection
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
"github.com/uptrace/bun/driver/pgdriver"
|
||||||
|
"github.com/uptrace/bun/dialect/pgdialect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PostgreSQL
|
||||||
|
func NewPostgresDB(dsn string) *bun.DB {
|
||||||
|
sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
|
||||||
|
db := bun.NewDB(sqldb, pgdialect.New())
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLite
|
||||||
|
import (
|
||||||
|
"github.com/uptrace/bun/driver/sqliteshim"
|
||||||
|
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSQLiteDB(path string) *bun.DB {
|
||||||
|
sqldb, _ := sql.Open(sqliteshim.ShimName, path)
|
||||||
|
db := bun.NewDB(sqldb, sqlitedialect.New())
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Model Definition
|
||||||
|
|
||||||
|
BUN models use struct tags:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
bun.BaseModel `bun:"table:users,alias:u"`
|
||||||
|
|
||||||
|
ID string `bun:"id,pk,type:varchar(36)"`
|
||||||
|
Username string `bun:"username,unique,notnull"`
|
||||||
|
Email string `bun:"email,unique,notnull"`
|
||||||
|
Password string `bun:"password,notnull"`
|
||||||
|
Role string `bun:"role,notnull,default:'user'"`
|
||||||
|
Active bool `bun:"active,notnull,default:true"`
|
||||||
|
CreatedAt time.Time `bun:"created_at,notnull,default:now()"`
|
||||||
|
UpdatedAt time.Time `bun:"updated_at,notnull,default:now()"`
|
||||||
|
DeletedAt bun.NullTime `bun:"deleted_at,soft_delete"`
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
APIKeys []*APIKey `bun:"rel:has-many,join:id=user_id"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CRUD Operations
|
||||||
|
|
||||||
|
### Create
|
||||||
|
|
||||||
|
```go
|
||||||
|
user := &User{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
Username: "john",
|
||||||
|
Email: "john@example.com",
|
||||||
|
Password: hashedPassword,
|
||||||
|
Role: "user",
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := db.NewInsert().
|
||||||
|
Model(user).
|
||||||
|
Exec(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Read
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Single record
|
||||||
|
user := new(User)
|
||||||
|
err := db.NewSelect().
|
||||||
|
Model(user).
|
||||||
|
Where("id = ?", userID).
|
||||||
|
Scan(ctx)
|
||||||
|
|
||||||
|
// Multiple records
|
||||||
|
var users []User
|
||||||
|
err := db.NewSelect().
|
||||||
|
Model(&users).
|
||||||
|
Where("active = ?", true).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Limit(10).
|
||||||
|
Scan(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Update specific fields
|
||||||
|
_, err := db.NewUpdate().
|
||||||
|
Model(&user).
|
||||||
|
Column("username", "email").
|
||||||
|
Where("id = ?", user.ID).
|
||||||
|
Exec(ctx)
|
||||||
|
|
||||||
|
// Update all fields
|
||||||
|
_, err := db.NewUpdate().
|
||||||
|
Model(&user).
|
||||||
|
WherePK().
|
||||||
|
Exec(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Soft delete (if model has soft_delete tag)
|
||||||
|
_, err := db.NewDelete().
|
||||||
|
Model(&user).
|
||||||
|
Where("id = ?", userID).
|
||||||
|
Exec(ctx)
|
||||||
|
|
||||||
|
// Hard delete
|
||||||
|
_, err := db.NewDelete().
|
||||||
|
Model(&user).
|
||||||
|
Where("id = ?", userID).
|
||||||
|
ForceDelete().
|
||||||
|
Exec(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Relationships
|
||||||
|
|
||||||
|
### Has-Many
|
||||||
|
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
bun.BaseModel `bun:"table:users"`
|
||||||
|
ID string `bun:"id,pk"`
|
||||||
|
APIKeys []*APIKey `bun:"rel:has-many,join:id=user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIKey struct {
|
||||||
|
bun.BaseModel `bun:"table:api_keys"`
|
||||||
|
ID string `bun:"id,pk"`
|
||||||
|
UserID string `bun:"user_id,notnull"`
|
||||||
|
User *User `bun:"rel:belongs-to,join:user_id=id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load with relations
|
||||||
|
var users []User
|
||||||
|
err := db.NewSelect().
|
||||||
|
Model(&users).
|
||||||
|
Relation("APIKeys").
|
||||||
|
Scan(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Belongs-To
|
||||||
|
|
||||||
|
```go
|
||||||
|
var apiKey APIKey
|
||||||
|
err := db.NewSelect().
|
||||||
|
Model(&apiKey).
|
||||||
|
Relation("User").
|
||||||
|
Where("api_key.id = ?", keyID).
|
||||||
|
Scan(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Many-to-Many
|
||||||
|
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
ID string `bun:"id,pk"`
|
||||||
|
Roles []*Role `bun:"m2m:user_roles,join:User=Role"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Role struct {
|
||||||
|
ID string `bun:"id,pk"`
|
||||||
|
Users []*User `bun:"m2m:user_roles,join:Role=User"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserRole struct {
|
||||||
|
UserID string `bun:"user_id,pk"`
|
||||||
|
RoleID string `bun:"role_id,pk"`
|
||||||
|
User *User `bun:"rel:belongs-to,join:user_id=id"`
|
||||||
|
Role *Role `bun:"rel:belongs-to,join:role_id=id"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Queries
|
||||||
|
|
||||||
|
### Filtering
|
||||||
|
|
||||||
|
```go
|
||||||
|
// WHERE clauses
|
||||||
|
err := db.NewSelect().
|
||||||
|
Model(&users).
|
||||||
|
Where("role = ?", "admin").
|
||||||
|
Where("active = ?", true).
|
||||||
|
Scan(ctx)
|
||||||
|
|
||||||
|
// OR conditions
|
||||||
|
err := db.NewSelect().
|
||||||
|
Model(&users).
|
||||||
|
WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||||
|
return q.
|
||||||
|
Where("role = ?", "admin").
|
||||||
|
Where("role = ?", "moderator")
|
||||||
|
}).
|
||||||
|
Scan(ctx)
|
||||||
|
|
||||||
|
// IN clause
|
||||||
|
err := db.NewSelect().
|
||||||
|
Model(&users).
|
||||||
|
Where("role IN (?)", bun.In([]string{"admin", "moderator"})).
|
||||||
|
Scan(ctx)
|
||||||
|
|
||||||
|
// LIKE
|
||||||
|
err := db.NewSelect().
|
||||||
|
Model(&users).
|
||||||
|
Where("username LIKE ?", "john%").
|
||||||
|
Scan(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sorting
|
||||||
|
|
||||||
|
```go
|
||||||
|
err := db.NewSelect().
|
||||||
|
Model(&users).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Order("username ASC").
|
||||||
|
Scan(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pagination
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Offset/Limit
|
||||||
|
err := db.NewSelect().
|
||||||
|
Model(&users).
|
||||||
|
Limit(20).
|
||||||
|
Offset(40).
|
||||||
|
Scan(ctx)
|
||||||
|
|
||||||
|
// Count
|
||||||
|
count, err := db.NewSelect().
|
||||||
|
Model((*User)(nil)).
|
||||||
|
Where("active = ?", true).
|
||||||
|
Count(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Aggregations
|
||||||
|
|
||||||
|
```go
|
||||||
|
// COUNT
|
||||||
|
count, err := db.NewSelect().
|
||||||
|
Model((*User)(nil)).
|
||||||
|
Count(ctx)
|
||||||
|
|
||||||
|
// GROUP BY
|
||||||
|
type Result struct {
|
||||||
|
Role string `bun:"role"`
|
||||||
|
Count int `bun:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []Result
|
||||||
|
err := db.NewSelect().
|
||||||
|
Model((*User)(nil)).
|
||||||
|
Column("role").
|
||||||
|
ColumnExpr("COUNT(*) as count").
|
||||||
|
Group("role").
|
||||||
|
Scan(ctx, &results)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transactions
|
||||||
|
|
||||||
|
```go
|
||||||
|
err := db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||||
|
// Create user
|
||||||
|
_, err := tx.NewInsert().
|
||||||
|
Model(&user).
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create API key
|
||||||
|
_, err = tx.NewInsert().
|
||||||
|
Model(&apiKey).
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migrations
|
||||||
|
|
||||||
|
### Create Table
|
||||||
|
|
||||||
|
```go
|
||||||
|
_, err := db.NewCreateTable().
|
||||||
|
Model((*User)(nil)).
|
||||||
|
IfNotExists().
|
||||||
|
Exec(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add Column
|
||||||
|
|
||||||
|
```go
|
||||||
|
_, err := db.NewAddColumn().
|
||||||
|
Model((*User)(nil)).
|
||||||
|
ColumnExpr("phone VARCHAR(50)").
|
||||||
|
Exec(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Drop Table
|
||||||
|
|
||||||
|
```go
|
||||||
|
_, err := db.NewDropTable().
|
||||||
|
Model((*User)(nil)).
|
||||||
|
IfExists().
|
||||||
|
Exec(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hooks
|
||||||
|
|
||||||
|
### Before/After Hooks
|
||||||
|
|
||||||
|
```go
|
||||||
|
var _ bun.BeforeAppendModelHook = (*User)(nil)
|
||||||
|
|
||||||
|
func (u *User) BeforeAppendModel(ctx context.Context, query bun.Query) error {
|
||||||
|
switch query.(type) {
|
||||||
|
case *bun.InsertQuery:
|
||||||
|
u.CreatedAt = time.Now()
|
||||||
|
case *bun.UpdateQuery:
|
||||||
|
u.UpdatedAt = time.Now()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ResolveSpec Integration
|
||||||
|
|
||||||
|
### Setup with BUN
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/restheadspec"
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create ResolveSpec handler with BUN
|
||||||
|
handler := restheadspec.NewHandlerWithBun(db)
|
||||||
|
|
||||||
|
// Setup routes (models are automatically discovered)
|
||||||
|
router := mux.NewRouter()
|
||||||
|
restheadspec.SetupMuxRoutes(router, handler, nil)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Queries
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Use BUN directly for complex queries
|
||||||
|
var results []struct {
|
||||||
|
UserID string
|
||||||
|
HookCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
err := db.NewSelect().
|
||||||
|
Model((*models.Hook)(nil)).
|
||||||
|
Column("user_id").
|
||||||
|
ColumnExpr("COUNT(*) as hook_count").
|
||||||
|
Group("user_id").
|
||||||
|
Scan(ctx, &results)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
### 1. Use Column Selection
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Don't load unnecessary columns
|
||||||
|
err := db.NewSelect().
|
||||||
|
Model(&users).
|
||||||
|
Column("id", "username", "email").
|
||||||
|
Scan(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Batch Operations
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Insert multiple records
|
||||||
|
_, err := db.NewInsert().
|
||||||
|
Model(&users).
|
||||||
|
Exec(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Use Indexes
|
||||||
|
|
||||||
|
```dbml
|
||||||
|
indexes {
|
||||||
|
(user_id) [name: 'idx_hooks_user_id']
|
||||||
|
(created_at) [name: 'idx_hooks_created_at']
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Connection Pooling
|
||||||
|
|
||||||
|
```go
|
||||||
|
sqldb.SetMaxOpenConns(25)
|
||||||
|
sqldb.SetMaxIdleConns(25)
|
||||||
|
sqldb.SetConnMaxLifetime(5 * time.Minute)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Prepared Statements
|
||||||
|
|
||||||
|
BUN automatically uses prepared statements for better performance.
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Repository Pattern
|
||||||
|
|
||||||
|
```go
|
||||||
|
type UserRepository struct {
|
||||||
|
db *bun.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserRepository(db *bun.DB) *UserRepository {
|
||||||
|
return &UserRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) GetByID(ctx context.Context, id string) (*models.User, error) {
|
||||||
|
user := new(models.User)
|
||||||
|
err := r.db.NewSelect().
|
||||||
|
Model(user).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Scan(ctx)
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*models.User, error) {
|
||||||
|
user := new(models.User)
|
||||||
|
err := r.db.NewSelect().
|
||||||
|
Model(user).
|
||||||
|
Where("username = ?", username).
|
||||||
|
Scan(ctx)
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Soft Deletes
|
||||||
|
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
bun.BaseModel `bun:"table:users"`
|
||||||
|
ID string `bun:"id,pk"`
|
||||||
|
DeletedAt bun.NullTime `bun:"deleted_at,soft_delete"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Automatically filters out soft-deleted records
|
||||||
|
var users []User
|
||||||
|
err := db.NewSelect().
|
||||||
|
Model(&users).
|
||||||
|
Scan(ctx)
|
||||||
|
|
||||||
|
// Include soft-deleted records
|
||||||
|
err := db.NewSelect().
|
||||||
|
Model(&users).
|
||||||
|
WhereAllWithDeleted().
|
||||||
|
Scan(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Tenancy
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Filter by user_id for all queries
|
||||||
|
err := db.NewSelect().
|
||||||
|
Model(&hooks).
|
||||||
|
Where("user_id = ?", currentUserID).
|
||||||
|
Scan(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/uptrace/bun/driver/pgdriver"
|
||||||
|
|
||||||
|
err := db.NewSelect().Model(&user).Where("id = ?", id).Scan(ctx)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, sql.ErrNoRows):
|
||||||
|
// Not found
|
||||||
|
case err != nil:
|
||||||
|
var pgErr pgdriver.Error
|
||||||
|
if errors.As(err, &pgErr) {
|
||||||
|
// PostgreSQL specific error
|
||||||
|
if pgErr.IntegrityViolation() {
|
||||||
|
// Handle constraint violation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
"github.com/uptrace/bun/dbfixture"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserRepository(t *testing.T) {
|
||||||
|
// Use in-memory SQLite for tests
|
||||||
|
db := NewSQLiteDB(":memory:")
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Create tables
|
||||||
|
ctx := context.Background()
|
||||||
|
_, err := db.NewCreateTable().
|
||||||
|
Model((*models.User)(nil)).
|
||||||
|
Exec(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test repository
|
||||||
|
repo := NewUserRepository(db)
|
||||||
|
user := &models.User{...}
|
||||||
|
err = repo.Create(ctx, user)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- BUN Documentation: https://bun.uptrace.dev/
|
||||||
|
- BUN GitHub: https://github.com/uptrace/bun
|
||||||
|
- PostgreSQL Driver: https://bun.uptrace.dev/postgres/
|
||||||
|
- SQLite Driver: https://bun.uptrace.dev/sqlite/
|
||||||
|
- ResolveSpec: https://github.com/bitechdev/ResolveSpec
|
||||||
259
tooldoc/CODE_GUIDELINES.md
Normal file
259
tooldoc/CODE_GUIDELINES.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
# Code Guidelines for WhatsHooked
|
||||||
|
|
||||||
|
## General Principles
|
||||||
|
|
||||||
|
- Write clean, idiomatic Go code following standard conventions
|
||||||
|
- Use meaningful variable and function names
|
||||||
|
- Keep functions small and focused on a single responsibility
|
||||||
|
- Document exported functions, types, and packages
|
||||||
|
- Handle errors explicitly, never ignore them
|
||||||
|
- Use context.Context for cancellation and timeout handling
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
whatshooked/
|
||||||
|
├── cmd/ # Command-line entry points
|
||||||
|
│ ├── server/ # HTTP server command
|
||||||
|
│ └── cli/ # CLI tools
|
||||||
|
├── pkg/ # Public packages
|
||||||
|
│ ├── auth/ # Authentication & authorization
|
||||||
|
│ ├── api/ # API endpoints
|
||||||
|
│ ├── cache/ # Caching layer
|
||||||
|
│ ├── config/ # Configuration management
|
||||||
|
│ ├── events/ # Event system
|
||||||
|
│ ├── eventlogger/ # Event logging
|
||||||
|
│ ├── handlers/ # HTTP handlers
|
||||||
|
│ ├── hooks/ # Webhook management
|
||||||
|
│ ├── logging/ # Structured logging
|
||||||
|
│ ├── storage/ # Database storage layer
|
||||||
|
│ ├── utils/ # Utility functions
|
||||||
|
│ ├── webserver/ # Web server with ResolveSpec
|
||||||
|
│ ├── whatsapp/ # WhatsApp integration
|
||||||
|
│ └── whatshooked/ # Core application logic
|
||||||
|
└── tooldoc/ # Tool & library documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
|
||||||
|
### Packages
|
||||||
|
- Use lowercase, single-word package names
|
||||||
|
- Use descriptive names that reflect the package's purpose
|
||||||
|
- Avoid generic names like `util` or `common` (use `utils` with specific subdirectories if needed)
|
||||||
|
|
||||||
|
### Files
|
||||||
|
- Use snake_case for file names: `user_service.go`, `auth_middleware.go`
|
||||||
|
- Test files: `user_service_test.go`
|
||||||
|
- Keep related functionality in the same file
|
||||||
|
|
||||||
|
### Variables & Functions
|
||||||
|
- Use camelCase for private: `userService`, `handleRequest`
|
||||||
|
- Use PascalCase for exported: `UserService`, `HandleRequest`
|
||||||
|
- Use descriptive names: prefer `userRepository` over `ur`
|
||||||
|
- Boolean variables should be prefixed: `isValid`, `hasPermission`, `canAccess`
|
||||||
|
|
||||||
|
### Constants
|
||||||
|
- Use PascalCase for exported: `DefaultTimeout`
|
||||||
|
- Use camelCase for private: `defaultTimeout`
|
||||||
|
- Group related constants together
|
||||||
|
|
||||||
|
### Interfaces
|
||||||
|
- Name interfaces with -er suffix when appropriate: `Reader`, `Writer`, `Handler`
|
||||||
|
- Use descriptive names: `UserRepository`, `AuthService`
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Wrap errors with context
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check specific errors
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom error types
|
||||||
|
type ValidationError struct {
|
||||||
|
Field string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ValidationError) Error() string {
|
||||||
|
return fmt.Sprintf("%s: %s", e.Field, e.Message)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
Use zerolog for structured logging:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Str("user_id", userID).
|
||||||
|
Msg("User logged in")
|
||||||
|
|
||||||
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Str("operation", "database_query").
|
||||||
|
Msg("Failed to query database")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Context Usage
|
||||||
|
|
||||||
|
Always pass context as the first parameter:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func ProcessRequest(ctx context.Context, userID string) error {
|
||||||
|
// Check for cancellation
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass context to downstream calls
|
||||||
|
user, err := userRepo.GetByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Write table-driven tests
|
||||||
|
- Use descriptive test names: `TestUserService_Create_WithValidData_Success`
|
||||||
|
- Mock external dependencies
|
||||||
|
- Aim for high coverage of business logic
|
||||||
|
- Use t.Run for subtests
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestUserService_Create(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input *User
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid user",
|
||||||
|
input: &User{Name: "John", Email: "john@example.com"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing email",
|
||||||
|
input: &User{Name: "John"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
service := NewUserService()
|
||||||
|
err := service.Create(tt.input)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Operations
|
||||||
|
|
||||||
|
- Use transactions for multiple related operations
|
||||||
|
- Always use prepared statements
|
||||||
|
- Handle NULL values properly
|
||||||
|
- Use meaningful struct tags
|
||||||
|
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
ID string `gorm:"primaryKey" json:"id"`
|
||||||
|
Name string `gorm:"not null" json:"name"`
|
||||||
|
Email string `gorm:"uniqueIndex;not null" json:"email"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ResolveSpec Integration
|
||||||
|
|
||||||
|
When using ResolveSpec:
|
||||||
|
- Register all models in the registry
|
||||||
|
- Use schema.table format: "public.users", "core.accounts"
|
||||||
|
- Implement hooks for auth and validation
|
||||||
|
- Use lifecycle hooks for audit logging
|
||||||
|
|
||||||
|
## API Design
|
||||||
|
|
||||||
|
- Use RESTful conventions
|
||||||
|
- Return appropriate HTTP status codes
|
||||||
|
- Include meaningful error messages
|
||||||
|
- Version your APIs: `/api/v1/users`
|
||||||
|
- Document all endpoints
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- Never log sensitive data (passwords, tokens, etc.)
|
||||||
|
- Validate all input
|
||||||
|
- Use parameterized queries
|
||||||
|
- Implement rate limiting
|
||||||
|
- Use HTTPS in production
|
||||||
|
- Sanitize user input
|
||||||
|
- Implement proper CORS policies
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Keep dependencies minimal
|
||||||
|
- Pin versions in go.mod
|
||||||
|
- Regularly update dependencies
|
||||||
|
- Document why each dependency is needed
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- Document all exported functions, types, and packages
|
||||||
|
- Use godoc format
|
||||||
|
- Include examples in documentation
|
||||||
|
- Keep README.md up to date
|
||||||
|
- Document configuration options
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Package auth provides authentication and authorization functionality.
|
||||||
|
package auth
|
||||||
|
|
||||||
|
// User represents a system user.
|
||||||
|
type User struct {
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate verifies user credentials and returns a token.
|
||||||
|
// Returns ErrInvalidCredentials if authentication fails.
|
||||||
|
func Authenticate(ctx context.Context, username, password string) (string, error) {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- Use connection pools for databases
|
||||||
|
- Implement caching where appropriate
|
||||||
|
- Avoid N+1 queries
|
||||||
|
- Use batch operations when possible
|
||||||
|
- Profile critical paths
|
||||||
|
- Set appropriate timeouts
|
||||||
|
|
||||||
|
## Git Workflow
|
||||||
|
|
||||||
|
- Write clear commit messages
|
||||||
|
- Keep commits atomic and focused
|
||||||
|
- Use conventional commits format
|
||||||
|
- Create feature branches
|
||||||
|
- Run tests before committing
|
||||||
|
- Review your own changes before pushing
|
||||||
316
tooldoc/ORANGURU.md
Normal file
316
tooldoc/ORANGURU.md
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
# Oranguru Integration Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Oranguru is a React component library that provides enhanced Mantine-based components with advanced features and state management capabilities. For WhatsHooked, we'll use it to build data grids and forms for the admin interface.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @warkypublic/oranguru
|
||||||
|
```
|
||||||
|
|
||||||
|
### Peer Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install react zustand @mantine/core @mantine/hooks @warkypublic/artemis-kit @warkypublic/zustandsyncstore use-sync-external-store
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### Enhanced Context Menus
|
||||||
|
Oranguru provides better menu positioning and visibility control than standard Mantine menus.
|
||||||
|
|
||||||
|
### Custom Rendering
|
||||||
|
Support for custom menu item renderers and complete menu rendering.
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
Uses Zustand for component state management with sync capabilities.
|
||||||
|
|
||||||
|
## Basic Setup
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { MantineBetterMenusProvider } from '@warkypublic/oranguru';
|
||||||
|
import { MantineProvider } from '@mantine/core';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<MantineProvider>
|
||||||
|
<MantineBetterMenusProvider>
|
||||||
|
{/* Your app content */}
|
||||||
|
</MantineBetterMenusProvider>
|
||||||
|
</MantineProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Context Menus
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useMantineBetterMenus } from '@warkypublic/oranguru';
|
||||||
|
|
||||||
|
function DataGrid() {
|
||||||
|
const { show, hide } = useMantineBetterMenus();
|
||||||
|
|
||||||
|
const handleContextMenu = (e: React.MouseEvent, record: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
show('record-menu', {
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
onClick: () => handleEdit(record)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Delete',
|
||||||
|
onClick: () => handleDelete(record)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isDivider: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'View Details',
|
||||||
|
onClick: () => handleViewDetails(record)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table>
|
||||||
|
{records.map(record => (
|
||||||
|
<tr key={record.id} onContextMenu={(e) => handleContextMenu(e, record)}>
|
||||||
|
<td>{record.name}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Async Actions
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const asyncMenuItem = {
|
||||||
|
label: 'Sync Data',
|
||||||
|
onClickAsync: async () => {
|
||||||
|
await fetch('/api/sync', { method: 'POST' });
|
||||||
|
// Shows loading state automatically
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Menu Items
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const customItem = {
|
||||||
|
renderer: ({ loading }: any) => (
|
||||||
|
<div style={{ padding: '8px 12px' }}>
|
||||||
|
{loading ? (
|
||||||
|
<Loader size="xs" />
|
||||||
|
) : (
|
||||||
|
<Group>
|
||||||
|
<IconCheck size={16} />
|
||||||
|
<Text>Custom Action</Text>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
onClickAsync: async () => {
|
||||||
|
await performAction();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with Data Grids
|
||||||
|
|
||||||
|
While Oranguru doesn't provide a built-in data grid component yet, it works excellently with Mantine's DataTable or custom table implementations:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useMantineBetterMenus } from '@warkypublic/oranguru';
|
||||||
|
import { DataTable } from '@mantine/datatable';
|
||||||
|
|
||||||
|
function UserGrid() {
|
||||||
|
const { show } = useMantineBetterMenus();
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ accessor: 'name', title: 'Name' },
|
||||||
|
{ accessor: 'email', title: 'Email' },
|
||||||
|
{ accessor: 'status', title: 'Status' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleRowContextMenu = (e: React.MouseEvent, user: User) => {
|
||||||
|
e.preventDefault();
|
||||||
|
show('user-menu', {
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Edit User',
|
||||||
|
onClick: () => navigate(`/users/${user.id}/edit`)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Deactivate',
|
||||||
|
onClickAsync: async () => {
|
||||||
|
await fetch(`/api/users/${user.id}/deactivate`, { method: 'POST' });
|
||||||
|
await refreshUsers();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isDivider: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Delete',
|
||||||
|
onClick: () => handleDelete(user.id)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
records={users}
|
||||||
|
onRowContextMenu={({ event, record }) => handleRowContextMenu(event, record)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Form Integration
|
||||||
|
|
||||||
|
For forms, use Mantine's form components with Oranguru's menu system for enhanced UX:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useForm } from '@mantine/form';
|
||||||
|
import { TextInput, Select, Button } from '@mantine/core';
|
||||||
|
|
||||||
|
function UserForm() {
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
role: ''
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
email: (value) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
|
||||||
|
name: (value) => (value.length < 2 ? 'Name too short' : null)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async (values: typeof form.values) => {
|
||||||
|
try {
|
||||||
|
await fetch('/api/users', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(values)
|
||||||
|
});
|
||||||
|
notifications.show({ message: 'User created successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
notifications.show({
|
||||||
|
message: 'Failed to create user',
|
||||||
|
color: 'red'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
|
<TextInput
|
||||||
|
label="Name"
|
||||||
|
placeholder="John Doe"
|
||||||
|
{...form.getInputProps('name')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Email"
|
||||||
|
placeholder="john@example.com"
|
||||||
|
{...form.getInputProps('email')}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Role"
|
||||||
|
data={['Admin', 'User', 'Viewer']}
|
||||||
|
{...form.getInputProps('role')}
|
||||||
|
/>
|
||||||
|
<Button type="submit">Create User</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Provider Placement**: Place MantineBetterMenusProvider at the root of your app
|
||||||
|
2. **Menu IDs**: Use descriptive, unique IDs for each menu type
|
||||||
|
3. **Context Menus**: Always prevent default on right-click events
|
||||||
|
4. **Loading States**: Use onClickAsync for async operations to get automatic loading states
|
||||||
|
5. **Custom Renderers**: Use custom renderers for complex menu items with icons, badges, etc.
|
||||||
|
6. **Portal Rendering**: Oranguru uses portals for proper z-index handling
|
||||||
|
7. **State Management**: Leverage Zustand store for complex menu state
|
||||||
|
8. **Accessibility**: Ensure keyboard navigation works with context menus
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Multi-Select Context Menu
|
||||||
|
```tsx
|
||||||
|
const handleBulkContextMenu = (e: React.MouseEvent, selectedIds: string[]) => {
|
||||||
|
e.preventDefault();
|
||||||
|
show('bulk-menu', {
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: `Delete ${selectedIds.length} items`,
|
||||||
|
onClickAsync: async () => {
|
||||||
|
await bulkDelete(selectedIds);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Export selection',
|
||||||
|
onClick: () => exportItems(selectedIds)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conditional Menu Items
|
||||||
|
```tsx
|
||||||
|
const menuItems = [
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
onClick: () => handleEdit(record)
|
||||||
|
},
|
||||||
|
canDelete(record) && {
|
||||||
|
label: 'Delete',
|
||||||
|
onClick: () => handleDelete(record)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isDivider: true
|
||||||
|
},
|
||||||
|
isAdmin && {
|
||||||
|
label: 'Admin Actions',
|
||||||
|
onClick: () => showAdminActions(record)
|
||||||
|
}
|
||||||
|
].filter(Boolean); // Remove falsy items
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nested Menus (Future Feature)
|
||||||
|
While not yet supported, Oranguru is designed to support nested menus in future versions.
|
||||||
|
|
||||||
|
## Integration with WhatsHooked
|
||||||
|
|
||||||
|
For WhatsHooked's admin interface:
|
||||||
|
|
||||||
|
1. **User Management Grid**: Use DataTable with context menus for user actions
|
||||||
|
2. **Hook Configuration**: Form with validation and async submission
|
||||||
|
3. **WhatsApp Account Management**: Grid with QR code display and pairing actions
|
||||||
|
4. **API Key Management**: Grid with copy-to-clipboard and revoke actions
|
||||||
|
5. **Event Logs**: Read-only grid with filtering and export
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Official Repository: https://git.warky.dev/wdevs/oranguru
|
||||||
|
- Mantine Documentation: https://mantine.dev
|
||||||
|
- Mantine DataTable: https://icflorescu.github.io/mantine-datatable/
|
||||||
504
tooldoc/REACT_MANTINE_TANSTACK.md
Normal file
504
tooldoc/REACT_MANTINE_TANSTACK.md
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
# React + Mantine + TanStack Start Integration Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
For WhatsHooked's admin interface, we'll use:
|
||||||
|
- **React 19**: Modern React with hooks and suspense
|
||||||
|
- **Mantine**: Component library for UI
|
||||||
|
- **TanStack Start**: Full-stack React framework with server-side rendering
|
||||||
|
- **Oranguru**: Enhanced Mantine components for grids and forms
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── app/
|
||||||
|
│ ├── routes/
|
||||||
|
│ │ ├── __root.tsx # Root layout
|
||||||
|
│ │ ├── index.tsx # Dashboard
|
||||||
|
│ │ ├── login.tsx # Login page
|
||||||
|
│ │ ├── users/
|
||||||
|
│ │ │ ├── index.tsx # User list
|
||||||
|
│ │ │ ├── new.tsx # Create user
|
||||||
|
│ │ │ └── $id/
|
||||||
|
│ │ │ ├── index.tsx # User details
|
||||||
|
│ │ │ └── edit.tsx # Edit user
|
||||||
|
│ │ ├── hooks/
|
||||||
|
│ │ │ ├── index.tsx # Hook list
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ └── accounts/
|
||||||
|
│ │ ├── index.tsx # WhatsApp accounts
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── Layout.tsx
|
||||||
|
│ │ ├── Navbar.tsx
|
||||||
|
│ │ ├── UserGrid.tsx
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── lib/
|
||||||
|
│ │ ├── api.ts # API client
|
||||||
|
│ │ ├── auth.ts # Auth utilities
|
||||||
|
│ │ └── types.ts # TypeScript types
|
||||||
|
│ └── styles/
|
||||||
|
│ └── global.css
|
||||||
|
├── public/
|
||||||
|
│ └── assets/
|
||||||
|
├── package.json
|
||||||
|
├── tsconfig.json
|
||||||
|
└── vite.config.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm create @tanstack/start@latest
|
||||||
|
cd whatshooked-admin
|
||||||
|
npm install @mantine/core @mantine/hooks @mantine/notifications @mantine/form @mantine/datatable
|
||||||
|
npm install @warkypublic/oranguru
|
||||||
|
npm install @tanstack/react-query axios
|
||||||
|
npm install -D @types/react @types/react-dom
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Setup
|
||||||
|
|
||||||
|
### app/routes/__root.tsx
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createRootRoute, Outlet } from '@tanstack/react-router';
|
||||||
|
import { MantineProvider } from '@mantine/core';
|
||||||
|
import { MantineBetterMenusProvider } from '@warkypublic/oranguru';
|
||||||
|
import { Notifications } from '@mantine/notifications';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
export const Route = createRootRoute({
|
||||||
|
component: () => (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<MantineProvider>
|
||||||
|
<MantineBetterMenusProvider>
|
||||||
|
<Notifications />
|
||||||
|
<Outlet />
|
||||||
|
</MantineBetterMenusProvider>
|
||||||
|
</MantineProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### app/lib/api.ts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8825/api/v1',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add auth token to requests
|
||||||
|
api.interceptors.request.use((config) => {
|
||||||
|
const token = localStorage.getItem('auth_token');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle auth errors
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem('auth_token');
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default api;
|
||||||
|
|
||||||
|
// API methods
|
||||||
|
export const authApi = {
|
||||||
|
login: (username: string, password: string) =>
|
||||||
|
api.post('/auth/login', { username, password }),
|
||||||
|
|
||||||
|
logout: () =>
|
||||||
|
api.post('/auth/logout'),
|
||||||
|
|
||||||
|
getProfile: () =>
|
||||||
|
api.get('/auth/profile'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usersApi = {
|
||||||
|
list: (params?: any) =>
|
||||||
|
api.get('/users', { params }),
|
||||||
|
|
||||||
|
get: (id: string) =>
|
||||||
|
api.get(`/users/${id}`),
|
||||||
|
|
||||||
|
create: (data: any) =>
|
||||||
|
api.post('/users', data),
|
||||||
|
|
||||||
|
update: (id: string, data: any) =>
|
||||||
|
api.put(`/users/${id}`, data),
|
||||||
|
|
||||||
|
delete: (id: string) =>
|
||||||
|
api.delete(`/users/${id}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hooksApi = {
|
||||||
|
list: () =>
|
||||||
|
api.get('/hooks'),
|
||||||
|
|
||||||
|
create: (data: any) =>
|
||||||
|
api.post('/hooks', data),
|
||||||
|
|
||||||
|
update: (id: string, data: any) =>
|
||||||
|
api.put(`/hooks/${id}`, data),
|
||||||
|
|
||||||
|
delete: (id: string) =>
|
||||||
|
api.delete(`/hooks/${id}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const accountsApi = {
|
||||||
|
list: () =>
|
||||||
|
api.get('/accounts'),
|
||||||
|
|
||||||
|
pair: (accountId: string) =>
|
||||||
|
api.post(`/accounts/${accountId}/pair`),
|
||||||
|
|
||||||
|
disconnect: (accountId: string) =>
|
||||||
|
api.post(`/accounts/${accountId}/disconnect`),
|
||||||
|
|
||||||
|
getQRCode: (accountId: string) =>
|
||||||
|
api.get(`/accounts/${accountId}/qr`, { responseType: 'blob' }),
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### app/routes/login.tsx
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||||
|
import { useForm } from '@mantine/form';
|
||||||
|
import { TextInput, PasswordInput, Button, Paper, Title, Container } from '@mantine/core';
|
||||||
|
import { notifications } from '@mantine/notifications';
|
||||||
|
import { authApi } from '../lib/api';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/login')({
|
||||||
|
component: LoginPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
function LoginPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
username: (value) => (value.length < 3 ? 'Username too short' : null),
|
||||||
|
password: (value) => (value.length < 6 ? 'Password too short' : null),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async (values: typeof form.values) => {
|
||||||
|
try {
|
||||||
|
const response = await authApi.login(values.username, values.password);
|
||||||
|
localStorage.setItem('auth_token', response.data.token);
|
||||||
|
notifications.show({
|
||||||
|
title: 'Success',
|
||||||
|
message: 'Logged in successfully',
|
||||||
|
color: 'green',
|
||||||
|
});
|
||||||
|
navigate({ to: '/' });
|
||||||
|
} catch (error) {
|
||||||
|
notifications.show({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Invalid credentials',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size={420} my={40}>
|
||||||
|
<Title ta="center">WhatsHooked Admin</Title>
|
||||||
|
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
||||||
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
|
<TextInput
|
||||||
|
label="Username"
|
||||||
|
placeholder="admin"
|
||||||
|
required
|
||||||
|
{...form.getInputProps('username')}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
label="Password"
|
||||||
|
placeholder="Your password"
|
||||||
|
required
|
||||||
|
mt="md"
|
||||||
|
{...form.getInputProps('password')}
|
||||||
|
/>
|
||||||
|
<Button fullWidth mt="xl" type="submit">
|
||||||
|
Sign in
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Grid with Oranguru
|
||||||
|
|
||||||
|
### app/routes/users/index.tsx
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { DataTable } from '@mantine/datatable';
|
||||||
|
import { useMantineBetterMenus } from '@warkypublic/oranguru';
|
||||||
|
import { Button, Group, Text } from '@mantine/core';
|
||||||
|
import { notifications } from '@mantine/notifications';
|
||||||
|
import { usersApi } from '../../lib/api';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/users/')({
|
||||||
|
component: UsersPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
function UsersPage() {
|
||||||
|
const { show } = useMantineBetterMenus();
|
||||||
|
|
||||||
|
const { data: users, isLoading, refetch } = useQuery({
|
||||||
|
queryKey: ['users'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await usersApi.list();
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleContextMenu = (e: React.MouseEvent, user: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
show('user-menu', {
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
onClick: () => navigate({ to: `/users/${user.id}/edit` }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'View Details',
|
||||||
|
onClick: () => navigate({ to: `/users/${user.id}` }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isDivider: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Delete',
|
||||||
|
onClickAsync: async () => {
|
||||||
|
await usersApi.delete(user.id);
|
||||||
|
notifications.show({
|
||||||
|
message: 'User deleted successfully',
|
||||||
|
color: 'green',
|
||||||
|
});
|
||||||
|
refetch();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ accessor: 'name', title: 'Name' },
|
||||||
|
{ accessor: 'email', title: 'Email' },
|
||||||
|
{ accessor: 'role', title: 'Role' },
|
||||||
|
{
|
||||||
|
accessor: 'actions',
|
||||||
|
title: '',
|
||||||
|
render: (user: any) => (
|
||||||
|
<Group gap="xs">
|
||||||
|
<Button size="xs" onClick={() => navigate({ to: `/users/${user.id}/edit` })}>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Group justify="space-between" mb="md">
|
||||||
|
<Text size="xl" fw={700}>Users</Text>
|
||||||
|
<Button onClick={() => navigate({ to: '/users/new' })}>
|
||||||
|
Create User
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
records={users || []}
|
||||||
|
fetching={isLoading}
|
||||||
|
onRowContextMenu={({ event, record }) => handleContextMenu(event, record)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Forms
|
||||||
|
|
||||||
|
### app/routes/users/new.tsx
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||||
|
import { useForm } from '@mantine/form';
|
||||||
|
import { TextInput, Select, Button, Paper } from '@mantine/core';
|
||||||
|
import { notifications } from '@mantine/notifications';
|
||||||
|
import { usersApi } from '../../lib/api';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/users/new')({
|
||||||
|
component: NewUserPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
function NewUserPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
role: 'user',
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
name: (value) => (value.length < 2 ? 'Name too short' : null),
|
||||||
|
email: (value) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
|
||||||
|
password: (value) => (value.length < 6 ? 'Password too short' : null),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async (values: typeof form.values) => {
|
||||||
|
try {
|
||||||
|
await usersApi.create(values);
|
||||||
|
notifications.show({
|
||||||
|
message: 'User created successfully',
|
||||||
|
color: 'green',
|
||||||
|
});
|
||||||
|
navigate({ to: '/users' });
|
||||||
|
} catch (error) {
|
||||||
|
notifications.show({
|
||||||
|
message: 'Failed to create user',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper p="md">
|
||||||
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
|
<TextInput
|
||||||
|
label="Name"
|
||||||
|
placeholder="John Doe"
|
||||||
|
required
|
||||||
|
{...form.getInputProps('name')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Email"
|
||||||
|
placeholder="john@example.com"
|
||||||
|
required
|
||||||
|
mt="md"
|
||||||
|
{...form.getInputProps('email')}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
label="Password"
|
||||||
|
placeholder="Password"
|
||||||
|
required
|
||||||
|
mt="md"
|
||||||
|
{...form.getInputProps('password')}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Role"
|
||||||
|
data={['admin', 'user', 'viewer']}
|
||||||
|
required
|
||||||
|
mt="md"
|
||||||
|
{...form.getInputProps('role')}
|
||||||
|
/>
|
||||||
|
<Group justify="flex-end" mt="xl">
|
||||||
|
<Button variant="subtle" onClick={() => navigate({ to: '/users' })}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">Create</Button>
|
||||||
|
</Group>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout with Navigation
|
||||||
|
|
||||||
|
### app/components/Layout.tsx
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { AppShell, NavLink, Group, Title } from '@mantine/core';
|
||||||
|
import { IconUsers, IconWebhook, IconBrandWhatsapp, IconKey } from '@tabler/icons-react';
|
||||||
|
import { Link, useLocation } from '@tanstack/react-router';
|
||||||
|
|
||||||
|
export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ icon: IconUsers, label: 'Users', to: '/users' },
|
||||||
|
{ icon: IconWebhook, label: 'Hooks', to: '/hooks' },
|
||||||
|
{ icon: IconBrandWhatsapp, label: 'WhatsApp Accounts', to: '/accounts' },
|
||||||
|
{ icon: IconKey, label: 'API Keys', to: '/api-keys' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
navbar={{ width: 250, breakpoint: 'sm' }}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Navbar p="md">
|
||||||
|
<Title order={3} mb="md">WhatsHooked</Title>
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<NavLink
|
||||||
|
key={item.to}
|
||||||
|
component={Link}
|
||||||
|
to={item.to}
|
||||||
|
label={item.label}
|
||||||
|
leftSection={<item.icon size={20} />}
|
||||||
|
active={location.pathname.startsWith(item.to)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</AppShell.Navbar>
|
||||||
|
|
||||||
|
<AppShell.Main>{children}</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Code Splitting**: Use lazy loading for routes
|
||||||
|
2. **Error Boundaries**: Wrap components in error boundaries
|
||||||
|
3. **Loading States**: Show loading indicators with Suspense
|
||||||
|
4. **Optimistic Updates**: Update UI before API response
|
||||||
|
5. **Form Validation**: Use Mantine form with validation
|
||||||
|
6. **Type Safety**: Use TypeScript for all API calls
|
||||||
|
7. **Query Invalidation**: Refetch data after mutations
|
||||||
|
8. **Auth Protection**: Protect routes with auth guards
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- TanStack Start: https://tanstack.com/start
|
||||||
|
- Mantine: https://mantine.dev
|
||||||
|
- TanStack Query: https://tanstack.com/query
|
||||||
|
- Oranguru: See ORANGURU.md
|
||||||
295
tooldoc/RELSPECGO.md
Normal file
295
tooldoc/RELSPECGO.md
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
# relspecgo - DBML to BUN Model Generator
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
relspecgo is a code generator that converts DBML (Database Markup Language) schema files into BUN ORM models for Go. It automates the creation of model structs with proper BUN tags, relationships, and indexes.
|
||||||
|
|
||||||
|
Repository: https://git.warky.dev/wdevs/relspecgo
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install git.warky.dev/wdevs/relspecgo@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via Makefile:
|
||||||
|
```bash
|
||||||
|
make install-relspecgo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
relspecgo generate --input=sql/schema.dbml --output=pkg/models --orm=bun
|
||||||
|
```
|
||||||
|
|
||||||
|
### Via Makefile
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make generate-models
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
1. Read `sql/schema.dbml`
|
||||||
|
2. Generate BUN models in `pkg/models/`
|
||||||
|
3. Create proper Go structs with BUN tags
|
||||||
|
|
||||||
|
## DBML Schema Format
|
||||||
|
|
||||||
|
### Table Definition
|
||||||
|
|
||||||
|
```dbml
|
||||||
|
Table users {
|
||||||
|
id varchar(36) [primary key]
|
||||||
|
username varchar(255) [unique, not null]
|
||||||
|
email varchar(255) [unique, not null]
|
||||||
|
password varchar(255) [not null]
|
||||||
|
role varchar(50) [not null, default: 'user']
|
||||||
|
active boolean [not null, default: true]
|
||||||
|
created_at timestamp [not null, default: `now()`]
|
||||||
|
updated_at timestamp [not null, default: `now()`]
|
||||||
|
deleted_at timestamp [null]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(deleted_at) [name: 'idx_users_deleted_at']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relationships
|
||||||
|
|
||||||
|
```dbml
|
||||||
|
Table api_keys {
|
||||||
|
id varchar(36) [primary key]
|
||||||
|
user_id varchar(36) [not null, ref: > users.id]
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit relationship with cascade delete
|
||||||
|
Ref: api_keys.user_id > users.id [delete: cascade]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported Field Types
|
||||||
|
|
||||||
|
- `varchar(n)` → `string`
|
||||||
|
- `text` → `string`
|
||||||
|
- `int`, `integer` → `int`
|
||||||
|
- `bigint` → `int64`
|
||||||
|
- `boolean`, `bool` → `bool`
|
||||||
|
- `timestamp`, `datetime` → `time.Time`
|
||||||
|
- `json`, `jsonb` → `json.RawMessage` or custom type
|
||||||
|
|
||||||
|
### Field Attributes
|
||||||
|
|
||||||
|
- `[primary key]` → BUN primary key tag
|
||||||
|
- `[not null]` → Required field
|
||||||
|
- `[unique]` → Unique constraint
|
||||||
|
- `[default: value]` → Default value
|
||||||
|
- `[note: 'text']` → Documentation comment
|
||||||
|
- `[ref: > table.column]` → Foreign key relationship
|
||||||
|
|
||||||
|
## Generated BUN Models
|
||||||
|
|
||||||
|
### Example Output
|
||||||
|
|
||||||
|
```go
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
bun.BaseModel `bun:"table:users,alias:u"`
|
||||||
|
|
||||||
|
ID string `bun:"id,pk,type:varchar(36)" json:"id"`
|
||||||
|
Username string `bun:"username,unique,notnull,type:varchar(255)" json:"username"`
|
||||||
|
Email string `bun:"email,unique,notnull,type:varchar(255)" json:"email"`
|
||||||
|
Password string `bun:"password,notnull,type:varchar(255)" json:"-"`
|
||||||
|
FullName string `bun:"full_name,type:varchar(255)" json:"full_name,omitempty"`
|
||||||
|
Role string `bun:"role,notnull,default:'user',type:varchar(50)" json:"role"`
|
||||||
|
Active bool `bun:"active,notnull,default:true" json:"active"`
|
||||||
|
CreatedAt time.Time `bun:"created_at,notnull,default:now()" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `bun:"updated_at,notnull,default:now()" json:"updated_at"`
|
||||||
|
DeletedAt time.Time `bun:"deleted_at,soft_delete" json:"deleted_at,omitempty"`
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
APIKeys []*APIKey `bun:"rel:has-many,join:id=user_id" json:"api_keys,omitempty"`
|
||||||
|
Hooks []*Hook `bun:"rel:has-many,join:id=user_id" json:"hooks,omitempty"`
|
||||||
|
WhatsAppAccounts []*WhatsAppAccount `bun:"rel:has-many,join:id=user_id" json:"whatsapp_accounts,omitempty"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### BUN Tags
|
||||||
|
|
||||||
|
- `bun:"table:users,alias:u"` - Table name and alias
|
||||||
|
- `bun:"id,pk"` - Primary key
|
||||||
|
- `bun:"username,unique"` - Unique constraint
|
||||||
|
- `bun:"password,notnull"` - NOT NULL constraint
|
||||||
|
- `bun:"role,default:'user'"` - Default value
|
||||||
|
- `bun:"type:varchar(255)"` - Explicit column type
|
||||||
|
- `bun:"deleted_at,soft_delete"` - Soft delete support
|
||||||
|
- `bun:"rel:has-many,join:id=user_id"` - Has-many relationship
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
sql/
|
||||||
|
├── schema.dbml # Main schema definition
|
||||||
|
├── postgres/ # PostgreSQL specific migrations
|
||||||
|
│ ├── 20240101_init.up.sql
|
||||||
|
│ └── 20240101_init.down.sql
|
||||||
|
└── sqlite/ # SQLite specific migrations
|
||||||
|
├── 20240101_init.up.sql
|
||||||
|
└── 20240101_init.down.sql
|
||||||
|
|
||||||
|
pkg/
|
||||||
|
└── models/ # Generated BUN models
|
||||||
|
├── user.go
|
||||||
|
├── api_key.go
|
||||||
|
├── hook.go
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### 1. Define Schema
|
||||||
|
|
||||||
|
Create or update `sql/schema.dbml`:
|
||||||
|
|
||||||
|
```dbml
|
||||||
|
Table products {
|
||||||
|
id int [primary key, increment]
|
||||||
|
name varchar(255) [not null]
|
||||||
|
price decimal(10,2) [not null]
|
||||||
|
created_at timestamp [not null, default: `now()`]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Generate Models
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make generate-models
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Create Migrations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make migrate-create NAME=add_products_table
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit generated migration files in `sql/postgres/` and `sql/sqlite/`
|
||||||
|
|
||||||
|
### 4. Run Migrations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make migrate-up
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Use in Code
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "git.warky.dev/wdevs/whatshooked/pkg/models"
|
||||||
|
|
||||||
|
// Query with BUN
|
||||||
|
var users []models.User
|
||||||
|
err := db.NewSelect().
|
||||||
|
Model(&users).
|
||||||
|
Relation("APIKeys").
|
||||||
|
Where("active = ?", true).
|
||||||
|
Scan(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Single Source of Truth**: Keep DBML as the source of truth for schema
|
||||||
|
2. **Regenerate After Changes**: Always run `make generate-models` after DBML changes
|
||||||
|
3. **Don't Edit Generated Files**: Modify DBML instead, then regenerate
|
||||||
|
4. **Version Control**: Commit both DBML and generated models
|
||||||
|
5. **Migrations**: Create migrations for schema changes
|
||||||
|
6. **Relationships**: Define relationships in DBML for proper code generation
|
||||||
|
7. **Indexes**: Specify indexes in DBML for performance
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate models from DBML
|
||||||
|
make generate-models
|
||||||
|
|
||||||
|
# Create new migration
|
||||||
|
make migrate-create NAME=add_users_table
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
make migrate-up
|
||||||
|
|
||||||
|
# Rollback migrations
|
||||||
|
make migrate-down
|
||||||
|
|
||||||
|
# Install relspecgo
|
||||||
|
make install-relspecgo
|
||||||
|
```
|
||||||
|
|
||||||
|
## DBML to SQL Conversion
|
||||||
|
|
||||||
|
relspecgo can also generate SQL from DBML:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
relspecgo sql --input=sql/schema.dbml --output=sql/postgres/schema.sql --dialect=postgres
|
||||||
|
relspecgo sql --input=sql/schema.dbml --output=sql/sqlite/schema.sql --dialect=sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advantages
|
||||||
|
|
||||||
|
1. **Type Safety**: Generated Go structs are type-safe
|
||||||
|
2. **Consistency**: Same schema definition for all models
|
||||||
|
3. **Documentation**: DBML serves as schema documentation
|
||||||
|
4. **Validation**: Catches schema errors before runtime
|
||||||
|
5. **IDE Support**: Full IDE autocomplete and type checking
|
||||||
|
6. **Relationships**: Automatic relationship setup
|
||||||
|
7. **Migration Friendly**: Easy to track schema changes
|
||||||
|
|
||||||
|
## Integration with ResolveSpec
|
||||||
|
|
||||||
|
Generated BUN models work seamlessly with ResolveSpec:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/restheadspec"
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create handler with BUN
|
||||||
|
handler := restheadspec.NewHandlerWithBun(db)
|
||||||
|
|
||||||
|
// Models are automatically discovered from BUN's table names
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Models Not Generated
|
||||||
|
|
||||||
|
- Check DBML syntax
|
||||||
|
- Ensure relspecgo is installed: `make install-relspecgo`
|
||||||
|
- Verify input/output paths
|
||||||
|
|
||||||
|
### Compilation Errors
|
||||||
|
|
||||||
|
- Run `go mod tidy` to update dependencies
|
||||||
|
- Check for missing imports
|
||||||
|
- Verify BUN version compatibility
|
||||||
|
|
||||||
|
### Relationship Issues
|
||||||
|
|
||||||
|
- Ensure foreign keys are properly defined in DBML
|
||||||
|
- Check `Ref:` declarations
|
||||||
|
- Verify join conditions
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- relspecgo: https://git.warky.dev/wdevs/relspecgo
|
||||||
|
- DBML Syntax: https://dbml.dbdiagram.io/docs/
|
||||||
|
- BUN ORM: https://bun.uptrace.dev/
|
||||||
|
- ResolveSpec: https://github.com/bitechdev/ResolveSpec
|
||||||
359
tooldoc/RESOLVESPEC.md
Normal file
359
tooldoc/RESOLVESPEC.md
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
# ResolveSpec Integration Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
ResolveSpec is a flexible REST API framework that provides GraphQL-like capabilities while maintaining REST simplicity. It offers two approaches:
|
||||||
|
1. **ResolveSpec** - Body-based API with JSON request options
|
||||||
|
2. **RestHeadSpec** - Header-based API where query options are passed via HTTP headers
|
||||||
|
|
||||||
|
For WhatsHooked, we'll use both approaches to provide maximum flexibility.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/bitechdev/ResolveSpec
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### Models
|
||||||
|
Models are Go structs that represent database tables. Use GORM tags for database mapping.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
ID string `gorm:"primaryKey" json:"id"`
|
||||||
|
Name string `gorm:"not null" json:"name"`
|
||||||
|
Email string `gorm:"uniqueIndex;not null" json:"email"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Registry
|
||||||
|
The registry maps schema.table names to Go models.
|
||||||
|
|
||||||
|
```go
|
||||||
|
handler := resolvespec.NewHandlerWithGORM(db)
|
||||||
|
handler.Registry.RegisterModel("public.users", &User{})
|
||||||
|
handler.Registry.RegisterModel("public.hooks", &Hook{})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Routing
|
||||||
|
ResolveSpec generates routes automatically for registered models:
|
||||||
|
- `/public/users` - Collection endpoints
|
||||||
|
- `/public/users/:id` - Individual resource endpoints
|
||||||
|
|
||||||
|
## ResolveSpec (Body-Based)
|
||||||
|
|
||||||
|
Request format:
|
||||||
|
```json
|
||||||
|
POST /public/users
|
||||||
|
{
|
||||||
|
"operation": "read|create|update|delete",
|
||||||
|
"data": {
|
||||||
|
// For create/update operations
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"columns": ["id", "name", "email"],
|
||||||
|
"filters": [
|
||||||
|
{"column": "status", "operator": "eq", "value": "active"}
|
||||||
|
],
|
||||||
|
"preload": ["hooks:id,url,events"],
|
||||||
|
"sort": ["-created_at", "+name"],
|
||||||
|
"limit": 50,
|
||||||
|
"offset": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setup with Gorilla Mux
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/resolvespec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupResolveSpec(db *gorm.DB) *mux.Router {
|
||||||
|
handler := resolvespec.NewHandlerWithGORM(db)
|
||||||
|
|
||||||
|
// Register models
|
||||||
|
handler.Registry.RegisterModel("public.users", &User{})
|
||||||
|
handler.Registry.RegisterModel("public.hooks", &Hook{})
|
||||||
|
|
||||||
|
// Setup routes
|
||||||
|
router := mux.NewRouter()
|
||||||
|
resolvespec.SetupMuxRoutes(router, handler, nil)
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## RestHeadSpec (Header-Based)
|
||||||
|
|
||||||
|
Request format:
|
||||||
|
```http
|
||||||
|
GET /public/users HTTP/1.1
|
||||||
|
X-Select-Fields: id,name,email
|
||||||
|
X-FieldFilter-Status: active
|
||||||
|
X-Preload: hooks:id,url,events
|
||||||
|
X-Sort: -created_at,+name
|
||||||
|
X-Limit: 50
|
||||||
|
X-Offset: 0
|
||||||
|
X-DetailApi: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setup with Gorilla Mux
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/restheadspec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupRestHeadSpec(db *gorm.DB) *mux.Router {
|
||||||
|
handler := restheadspec.NewHandlerWithGORM(db)
|
||||||
|
|
||||||
|
// Register models
|
||||||
|
handler.Registry.RegisterModel("public.users", &User{})
|
||||||
|
handler.Registry.RegisterModel("public.hooks", &Hook{})
|
||||||
|
|
||||||
|
// Setup routes
|
||||||
|
router := mux.NewRouter()
|
||||||
|
restheadspec.SetupMuxRoutes(router, handler, nil)
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle Hooks (RestHeadSpec)
|
||||||
|
|
||||||
|
Add hooks for authentication, validation, and audit logging:
|
||||||
|
|
||||||
|
```go
|
||||||
|
handler.OnBeforeRead(func(ctx context.Context, req *restheadspec.Request) error {
|
||||||
|
// Check permissions
|
||||||
|
userID := ctx.Value("user_id").(string)
|
||||||
|
if !canRead(userID, req.Schema, req.Entity) {
|
||||||
|
return fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.OnAfterCreate(func(ctx context.Context, req *restheadspec.Request, result interface{}) error {
|
||||||
|
// Audit log
|
||||||
|
log.Info().
|
||||||
|
Str("user_id", ctx.Value("user_id").(string)).
|
||||||
|
Str("entity", req.Entity).
|
||||||
|
Msg("Created record")
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication Integration
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Middleware to extract user from JWT
|
||||||
|
func AuthMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := r.Header.Get("Authorization")
|
||||||
|
user, err := ValidateToken(token)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.WithValue(r.Context(), "user_id", user.ID)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply to routes
|
||||||
|
router.Use(AuthMiddleware)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Filtering
|
||||||
|
|
||||||
|
### Field Filters (RestHeadSpec)
|
||||||
|
```http
|
||||||
|
X-FieldFilter-Status: active
|
||||||
|
X-FieldFilter-Age: 18
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search Operators (RestHeadSpec)
|
||||||
|
```http
|
||||||
|
X-SearchOp-Gte-Age: 18
|
||||||
|
X-SearchOp-Like-Name: john
|
||||||
|
```
|
||||||
|
|
||||||
|
### Body Filters (ResolveSpec)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"options": {
|
||||||
|
"filters": [
|
||||||
|
{"column": "status", "operator": "eq", "value": "active"},
|
||||||
|
{"column": "age", "operator": "gte", "value": 18},
|
||||||
|
{"column": "name", "operator": "like", "value": "john%"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pagination
|
||||||
|
|
||||||
|
### Offset-Based
|
||||||
|
```http
|
||||||
|
X-Limit: 50
|
||||||
|
X-Offset: 100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cursor-Based (RestHeadSpec)
|
||||||
|
```http
|
||||||
|
X-Cursor: eyJpZCI6IjEyMyIsImNyZWF0ZWRfYXQiOiIyMDI0LTAxLTAxIn0=
|
||||||
|
X-Limit: 50
|
||||||
|
```
|
||||||
|
|
||||||
|
## Preloading Relationships
|
||||||
|
|
||||||
|
Load related entities with custom columns:
|
||||||
|
|
||||||
|
```http
|
||||||
|
X-Preload: hooks:id,url,events,posts:id,title
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"options": {
|
||||||
|
"preload": ["hooks:id,url,events", "posts:id,title"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sorting
|
||||||
|
|
||||||
|
```http
|
||||||
|
X-Sort: -created_at,+name
|
||||||
|
```
|
||||||
|
|
||||||
|
Prefix with `-` for descending, `+` for ascending.
|
||||||
|
|
||||||
|
## Response Formats (RestHeadSpec)
|
||||||
|
|
||||||
|
### Simple Format (default)
|
||||||
|
```http
|
||||||
|
X-DetailApi: false
|
||||||
|
```
|
||||||
|
Returns: `[{...}, {...}]`
|
||||||
|
|
||||||
|
### Detailed Format
|
||||||
|
```http
|
||||||
|
X-DetailApi: true
|
||||||
|
```
|
||||||
|
Returns:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [{...}, {...}],
|
||||||
|
"meta": {
|
||||||
|
"total": 100,
|
||||||
|
"limit": 50,
|
||||||
|
"offset": 0,
|
||||||
|
"cursor": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CORS Configuration
|
||||||
|
|
||||||
|
```go
|
||||||
|
corsConfig := &common.CORSConfig{
|
||||||
|
AllowedOrigins: []string{"*"},
|
||||||
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||||
|
AllowedHeaders: []string{"*"},
|
||||||
|
ExposedHeaders: []string{"X-Total-Count", "X-Cursor"},
|
||||||
|
}
|
||||||
|
|
||||||
|
restheadspec.SetupMuxRoutes(router, handler, corsConfig)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
ResolveSpec returns standard HTTP error codes:
|
||||||
|
- 200: Success
|
||||||
|
- 400: Bad Request
|
||||||
|
- 401: Unauthorized
|
||||||
|
- 404: Not Found
|
||||||
|
- 500: Internal Server Error
|
||||||
|
|
||||||
|
Error response format:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "error message",
|
||||||
|
"details": "additional context"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Register models before routes**: Always register all models before calling SetupMuxRoutes
|
||||||
|
2. **Use lifecycle hooks**: Implement authentication and validation in hooks
|
||||||
|
3. **Schema naming**: Use `schema.table` format consistently
|
||||||
|
4. **Transactions**: Use database transactions for multi-record operations
|
||||||
|
5. **Validation**: Validate input in OnBeforeCreate/OnBeforeUpdate hooks
|
||||||
|
6. **Audit logging**: Use OnAfter* hooks for audit trails
|
||||||
|
7. **Performance**: Use preloading instead of N+1 queries
|
||||||
|
8. **Security**: Implement row-level security in hooks
|
||||||
|
9. **Rate limiting**: Add rate limiting middleware
|
||||||
|
10. **Monitoring**: Log all operations for monitoring
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### User Filtering (Multi-tenancy)
|
||||||
|
```go
|
||||||
|
handler.OnBeforeRead(func(ctx context.Context, req *restheadspec.Request) error {
|
||||||
|
userID := ctx.Value("user_id").(string)
|
||||||
|
|
||||||
|
// Add user_id filter
|
||||||
|
req.Options.Filters = append(req.Options.Filters, Filter{
|
||||||
|
Column: "user_id",
|
||||||
|
Operator: "eq",
|
||||||
|
Value: userID,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Soft Deletes
|
||||||
|
```go
|
||||||
|
handler.OnBeforeDelete(func(ctx context.Context, req *restheadspec.Request) error {
|
||||||
|
// Convert to update with deleted_at
|
||||||
|
req.Operation = "update"
|
||||||
|
req.Data = map[string]interface{}{
|
||||||
|
"deleted_at": time.Now(),
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
```go
|
||||||
|
handler.OnBeforeCreate(func(ctx context.Context, req *restheadspec.Request) error {
|
||||||
|
user := req.Data.(*User)
|
||||||
|
|
||||||
|
if user.Email == "" {
|
||||||
|
return fmt.Errorf("email is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValidEmail(user.Email) {
|
||||||
|
return fmt.Errorf("invalid email format")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Official Docs: https://github.com/bitechdev/ResolveSpec
|
||||||
|
- ResolveSpec README: /pkg/resolvespec/README.md
|
||||||
|
- RestHeadSpec README: /pkg/restheadspec/README.md
|
||||||
Reference in New Issue
Block a user