Initial commit: Project foundation
- Backend: Go API server with Gin framework - Frontend: React setup (placeholder) - ML Service: Python FastAPI skeleton - Docker Compose: Full stack configuration - Database: PostgreSQL schema with migrations - Documentation: Implementation plan and README Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
22
backend/.gitignore
vendored
Normal file
22
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# Go
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.test
|
||||
*.out
|
||||
go.work
|
||||
|
||||
# Binary
|
||||
/bin/
|
||||
/build/
|
||||
server
|
||||
|
||||
# Vendor
|
||||
vendor/
|
||||
|
||||
# vmalert rules (generated)
|
||||
configs/vmalert-rules/*.yml
|
||||
configs/vmalert-rules/*.yaml
|
||||
!configs/vmalert-rules/.gitkeep
|
||||
40
backend/Dockerfile
Normal file
40
backend/Dockerfile
Normal file
@@ -0,0 +1,40 @@
|
||||
# Build stage
|
||||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache git
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy go mod files
|
||||
COPY go.mod go.sum* ./
|
||||
|
||||
# Download dependencies
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server ./cmd/server
|
||||
|
||||
# Final stage
|
||||
FROM alpine:latest
|
||||
|
||||
# Install ca-certificates for HTTPS
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
# Copy binary from builder
|
||||
COPY --from=builder /app/server .
|
||||
|
||||
# Copy configuration
|
||||
COPY --from=builder /app/configs ./configs
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Run the application
|
||||
CMD ["./server"]
|
||||
39
backend/configs/config.yaml
Normal file
39
backend/configs/config.yaml
Normal file
@@ -0,0 +1,39 @@
|
||||
server:
|
||||
port: 8080
|
||||
read_timeout: 30s
|
||||
write_timeout: 30s
|
||||
|
||||
database:
|
||||
host: ${DB_HOST:postgres}
|
||||
port: ${DB_PORT:5432}
|
||||
name: ${DB_NAME:vlogs_manager}
|
||||
user: ${DB_USER:vlogs}
|
||||
password: ${DB_PASSWORD}
|
||||
max_connections: 25
|
||||
|
||||
victorialogs:
|
||||
url: ${VLOGS_URL:http://victorialogs:9428}
|
||||
timeout: 30s
|
||||
max_query_size: 10000
|
||||
|
||||
vmalert:
|
||||
url: ${VMALERT_URL:http://vmalert:8880}
|
||||
rules_dir: ${VMALERT_RULES_DIR:/app/rules}
|
||||
reload_endpoint: "/-/reload"
|
||||
|
||||
ml_service:
|
||||
url: ${ML_SERVICE_URL:http://ml-service:8000}
|
||||
timeout: 60s
|
||||
|
||||
auth:
|
||||
jwt_secret: ${JWT_SECRET}
|
||||
access_token_duration: 15m
|
||||
refresh_token_duration: 168h
|
||||
|
||||
rate_limiting:
|
||||
enabled: true
|
||||
requests_per_minute:
|
||||
admin: 1000
|
||||
editor: 500
|
||||
analyst: 300
|
||||
viewer: 200
|
||||
0
backend/configs/vmalert-rules/.gitkeep
Normal file
0
backend/configs/vmalert-rules/.gitkeep
Normal file
12
backend/go.mod
Normal file
12
backend/go.mod
Normal file
@@ -0,0 +1,12 @@
|
||||
module github.com/yourusername/victorialogs-manager
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/lib/pq v1.10.9
|
||||
golang.org/x/crypto v0.18.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
186
backend/internal/api/router.go
Normal file
186
backend/internal/api/router.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yourusername/victorialogs-manager/internal/config"
|
||||
)
|
||||
|
||||
// NewRouter creates and configures the main router
|
||||
func NewRouter(cfg *config.Config, db *sql.DB) *gin.Engine {
|
||||
// Set Gin mode
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
router := gin.Default()
|
||||
|
||||
// CORS middleware
|
||||
router.Use(corsMiddleware())
|
||||
|
||||
// Health check endpoint
|
||||
router.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
"service": "victorialogs-manager",
|
||||
})
|
||||
})
|
||||
|
||||
// API v1 routes
|
||||
v1 := router.Group("/api/v1")
|
||||
{
|
||||
// Authentication routes (public)
|
||||
auth := v1.Group("/auth")
|
||||
{
|
||||
auth.POST("/login", func(c *gin.Context) {
|
||||
// TODO: Implement login handler
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
auth.POST("/refresh", func(c *gin.Context) {
|
||||
// TODO: Implement refresh handler
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
auth.GET("/me", func(c *gin.Context) {
|
||||
// TODO: Implement me handler (requires auth)
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
}
|
||||
|
||||
// Protected routes (require authentication)
|
||||
// TODO: Add auth middleware here
|
||||
|
||||
// User management routes
|
||||
users := v1.Group("/users")
|
||||
{
|
||||
users.GET("", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
users.POST("", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
users.GET("/:id", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
users.PUT("/:id", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
users.DELETE("/:id", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
}
|
||||
|
||||
// Log querying routes
|
||||
logs := v1.Group("/logs")
|
||||
{
|
||||
logs.POST("/query", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
logs.POST("/tail", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
logs.GET("/facets", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
logs.POST("/stats", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
logs.POST("/export", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
}
|
||||
|
||||
// Alert management routes
|
||||
alerts := v1.Group("/alerts")
|
||||
{
|
||||
alerts.GET("/rules", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
alerts.POST("/rules", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
alerts.GET("/rules/:id", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
alerts.PUT("/rules/:id", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
alerts.DELETE("/rules/:id", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
alerts.POST("/reload", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
alerts.GET("/active", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
}
|
||||
|
||||
// Pattern detection routes
|
||||
patterns := v1.Group("/patterns")
|
||||
{
|
||||
patterns.GET("", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
patterns.POST("", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
patterns.PUT("/:id", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
patterns.DELETE("/:id", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
patterns.POST("/detect", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
patterns.POST("/ml/train", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
patterns.POST("/ml/detect", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
}
|
||||
|
||||
// Report routes
|
||||
reports := v1.Group("/reports")
|
||||
{
|
||||
reports.GET("", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
reports.POST("", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
reports.GET("/:id", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
reports.PUT("/:id", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
reports.DELETE("/:id", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
reports.POST("/:id/execute", func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
// corsMiddleware handles CORS headers
|
||||
func corsMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
106
backend/internal/config/config.go
Normal file
106
backend/internal/config/config.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
VictoriaLogs VictoriaLogsConfig `yaml:"victorialogs"`
|
||||
VMAlert VMAlertConfig `yaml:"vmalert"`
|
||||
MLService MLServiceConfig `yaml:"ml_service"`
|
||||
Auth AuthConfig `yaml:"auth"`
|
||||
RateLimiting RateLimitingConfig `yaml:"rate_limiting"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port int `yaml:"port"`
|
||||
ReadTimeout time.Duration `yaml:"read_timeout"`
|
||||
WriteTimeout time.Duration `yaml:"write_timeout"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
Name string `yaml:"name"`
|
||||
User string `yaml:"user"`
|
||||
Password string `yaml:"password"`
|
||||
MaxConnections int `yaml:"max_connections"`
|
||||
}
|
||||
|
||||
type VictoriaLogsConfig struct {
|
||||
URL string `yaml:"url"`
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
MaxQuerySize int `yaml:"max_query_size"`
|
||||
}
|
||||
|
||||
type VMAlertConfig struct {
|
||||
URL string `yaml:"url"`
|
||||
RulesDir string `yaml:"rules_dir"`
|
||||
ReloadEndpoint string `yaml:"reload_endpoint"`
|
||||
}
|
||||
|
||||
type MLServiceConfig struct {
|
||||
URL string `yaml:"url"`
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
JWTSecret string `yaml:"jwt_secret"`
|
||||
AccessTokenDuration time.Duration `yaml:"access_token_duration"`
|
||||
RefreshTokenDuration time.Duration `yaml:"refresh_token_duration"`
|
||||
}
|
||||
|
||||
type RateLimitingConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
RequestsPerMinute map[string]int `yaml:"requests_per_minute"`
|
||||
}
|
||||
|
||||
// Load loads configuration from YAML file and environment variables
|
||||
func Load(configPath string) (*Config, error) {
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
// Expand environment variables in config
|
||||
expandedData := []byte(os.ExpandEnv(string(data)))
|
||||
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(expandedData, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if err := cfg.validate(); err != nil {
|
||||
return nil, fmt.Errorf("config validation failed: %w", err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func (c *Config) validate() error {
|
||||
if c.Database.Password == "" {
|
||||
return fmt.Errorf("database password is required")
|
||||
}
|
||||
if c.Auth.JWTSecret == "" {
|
||||
return fmt.Errorf("JWT secret is required")
|
||||
}
|
||||
if len(c.Auth.JWTSecret) < 32 {
|
||||
return fmt.Errorf("JWT secret must be at least 32 characters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDSN returns the PostgreSQL connection string
|
||||
func (c *DatabaseConfig) GetDSN() string {
|
||||
return fmt.Sprintf(
|
||||
"host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
|
||||
c.Host, c.Port, c.User, c.Password, c.Name,
|
||||
)
|
||||
}
|
||||
78
backend/internal/models/user.go
Normal file
78
backend/internal/models/user.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// Role represents user roles
|
||||
type Role string
|
||||
|
||||
const (
|
||||
RoleAdmin Role = "admin"
|
||||
RoleEditor Role = "editor"
|
||||
RoleAnalyst Role = "analyst"
|
||||
RoleViewer Role = "viewer"
|
||||
)
|
||||
|
||||
// Permission represents a specific permission
|
||||
type Permission string
|
||||
|
||||
const (
|
||||
PermViewLogs Permission = "logs:view"
|
||||
PermExportLogs Permission = "logs:export"
|
||||
PermManageAlerts Permission = "alerts:manage"
|
||||
PermViewAlerts Permission = "alerts:view"
|
||||
PermManagePatterns Permission = "patterns:manage"
|
||||
PermRunML Permission = "patterns:ml"
|
||||
PermManageReports Permission = "reports:manage"
|
||||
PermManageUsers Permission = "users:manage"
|
||||
)
|
||||
|
||||
// RolePermissions maps roles to their permissions
|
||||
var RolePermissions = map[Role][]Permission{
|
||||
RoleAdmin: {
|
||||
PermViewLogs, PermExportLogs, PermManageAlerts,
|
||||
PermViewAlerts, PermManagePatterns, PermRunML,
|
||||
PermManageReports, PermManageUsers,
|
||||
},
|
||||
RoleEditor: {
|
||||
PermViewLogs, PermExportLogs, PermManageAlerts,
|
||||
PermViewAlerts, PermManagePatterns, PermManageReports,
|
||||
},
|
||||
RoleAnalyst: {
|
||||
PermViewLogs, PermViewAlerts, PermManagePatterns, PermRunML,
|
||||
},
|
||||
RoleViewer: {
|
||||
PermViewLogs, PermViewAlerts,
|
||||
},
|
||||
}
|
||||
|
||||
// User represents a user in the system
|
||||
type User struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
Username string `json:"username" db:"username"`
|
||||
Email string `json:"email" db:"email"`
|
||||
PasswordHash string `json:"-" db:"password_hash"`
|
||||
Role Role `json:"role" db:"role"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// HasPermission checks if the user has a specific permission
|
||||
func (u *User) HasPermission(perm Permission) bool {
|
||||
permissions, ok := RolePermissions[u.Role]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, p := range permissions {
|
||||
if p == perm {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetPermissions returns all permissions for the user's role
|
||||
func (u *User) GetPermissions() []Permission {
|
||||
return RolePermissions[u.Role]
|
||||
}
|
||||
132
backend/migrations/001_init.sql
Normal file
132
backend/migrations/001_init.sql
Normal file
@@ -0,0 +1,132 @@
|
||||
-- Create users table
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
username VARCHAR(255) UNIQUE NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(50) NOT NULL CHECK (role IN ('admin', 'editor', 'analyst', 'viewer')),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create index on username and email for faster lookups
|
||||
CREATE INDEX idx_users_username ON users(username);
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
CREATE INDEX idx_users_role ON users(role);
|
||||
|
||||
-- Create alert_rules table
|
||||
CREATE TABLE IF NOT EXISTS alert_rules (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
group_name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(50) NOT NULL CHECK (type IN ('alert', 'record')),
|
||||
expression TEXT NOT NULL,
|
||||
duration VARCHAR(50),
|
||||
labels JSONB DEFAULT '{}',
|
||||
annotations JSONB DEFAULT '{}',
|
||||
severity VARCHAR(50),
|
||||
enabled BOOLEAN DEFAULT true,
|
||||
yaml_content TEXT NOT NULL,
|
||||
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create indexes for alert_rules
|
||||
CREATE INDEX idx_alert_rules_group_name ON alert_rules(group_name);
|
||||
CREATE INDEX idx_alert_rules_enabled ON alert_rules(enabled);
|
||||
CREATE INDEX idx_alert_rules_created_by ON alert_rules(created_by);
|
||||
|
||||
-- Create patterns table
|
||||
CREATE TABLE IF NOT EXISTS patterns (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(50) NOT NULL CHECK (type IN ('regex', 'ml')),
|
||||
description TEXT,
|
||||
config JSONB NOT NULL,
|
||||
severity VARCHAR(50),
|
||||
tags TEXT[],
|
||||
enabled BOOLEAN DEFAULT true,
|
||||
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create indexes for patterns
|
||||
CREATE INDEX idx_patterns_type ON patterns(type);
|
||||
CREATE INDEX idx_patterns_enabled ON patterns(enabled);
|
||||
CREATE INDEX idx_patterns_created_by ON patterns(created_by);
|
||||
|
||||
-- Create reports table
|
||||
CREATE TABLE IF NOT EXISTS reports (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
config JSONB NOT NULL,
|
||||
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create index for reports
|
||||
CREATE INDEX idx_reports_created_by ON reports(created_by);
|
||||
|
||||
-- Create audit_logs table for tracking sensitive operations
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
action VARCHAR(255) NOT NULL,
|
||||
resource_type VARCHAR(100),
|
||||
resource_id UUID,
|
||||
details JSONB,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create indexes for audit_logs
|
||||
CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);
|
||||
CREATE INDEX idx_audit_logs_action ON audit_logs(action);
|
||||
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);
|
||||
|
||||
-- Create function to update updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- Create triggers for updated_at
|
||||
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_alert_rules_updated_at BEFORE UPDATE ON alert_rules
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_patterns_updated_at BEFORE UPDATE ON patterns
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_reports_updated_at BEFORE UPDATE ON reports
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Insert default admin user (password: admin123 - CHANGE IN PRODUCTION!)
|
||||
-- Password hash is bcrypt hash of "admin123"
|
||||
INSERT INTO users (username, email, password_hash, role)
|
||||
VALUES (
|
||||
'admin',
|
||||
'admin@example.com',
|
||||
'$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5lW.HKCFcVpSK',
|
||||
'admin'
|
||||
) ON CONFLICT (username) DO NOTHING;
|
||||
|
||||
-- Insert sample viewer user (password: viewer123)
|
||||
INSERT INTO users (username, email, password_hash, role)
|
||||
VALUES (
|
||||
'viewer',
|
||||
'viewer@example.com',
|
||||
'$2a$12$3pYKzLw9z1FqN8QdKxI.J.nVZY5TqJxM7Y.qQHkZKZhzJ8qQnPZ8W',
|
||||
'viewer'
|
||||
) ON CONFLICT (username) DO NOTHING;
|
||||
Reference in New Issue
Block a user