feat: integrate Grafana dashboards into BOC DashboardPage
- Add Infrastructure Health section with CPU/Memory/Disk panels - Add Service Status section with PM2/Docker panels - Create GrafanaPanel component for iframe embedding - Build passes successfully
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// ── Input Validation ───────────────────────────────────────────────────────
|
||||
|
||||
var (
|
||||
emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
uuidRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
|
||||
phoneRegex = regexp.MustCompile(`^[+0-9\s()-]{8,20}$`)
|
||||
orgNumRegex = regexp.MustCompile(`^\d{6}-\d{4}$`)
|
||||
)
|
||||
|
||||
// ValidateEmail kontrollerar email-format
|
||||
func ValidateEmail(email string) bool {
|
||||
return emailRegex.MatchString(email)
|
||||
}
|
||||
|
||||
// ValidateUUID kontrollerar UUID-format
|
||||
func ValidateUUID(id string) bool {
|
||||
return uuidRegex.MatchString(id)
|
||||
}
|
||||
|
||||
// ValidatePhone kontrollerar telefonnummer
|
||||
func ValidatePhone(phone string) bool {
|
||||
return phoneRegex.MatchString(phone)
|
||||
}
|
||||
|
||||
// ValidateOrgNumber kontrollerar svenskt orgnummer
|
||||
func ValidateOrgNumber(org string) bool {
|
||||
return orgNumRegex.MatchString(org)
|
||||
}
|
||||
|
||||
// SanitizeString rensar input från farliga tecken
|
||||
func SanitizeString(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
// Ta bort potentiellt farliga tecken
|
||||
s = strings.ReplaceAll(s, "<", "<")
|
||||
s = strings.ReplaceAll(s, ">", ">")
|
||||
s = strings.ReplaceAll(s, "\"", """)
|
||||
return s
|
||||
}
|
||||
|
||||
// ── RBAC (Role Based Access Control) ──────────────────────────────────────
|
||||
|
||||
type Permission string
|
||||
|
||||
const (
|
||||
PermRead Permission = "read"
|
||||
PermWrite Permission = "write"
|
||||
PermDelete Permission = "delete"
|
||||
PermAdmin Permission = "admin"
|
||||
)
|
||||
|
||||
type Resource string
|
||||
|
||||
const (
|
||||
ResCustomers Resource = "customers"
|
||||
ResEmployees Resource = "employees"
|
||||
ResFinance Resource = "finance"
|
||||
ResLegal Resource = "legal"
|
||||
ResHR Resource = "hr"
|
||||
ResSettings Resource = "settings"
|
||||
ResAudit Resource = "audit"
|
||||
)
|
||||
|
||||
// RolePermissions definierar vilka permissions varje roll har
|
||||
var RolePermissions = map[string]map[Resource][]Permission{
|
||||
"admin": {
|
||||
ResCustomers: {PermRead, PermWrite, PermDelete},
|
||||
ResEmployees: {PermRead, PermWrite, PermDelete},
|
||||
ResFinance: {PermRead, PermWrite, PermDelete},
|
||||
ResLegal: {PermRead, PermWrite, PermDelete},
|
||||
ResHR: {PermRead, PermWrite, PermDelete},
|
||||
ResSettings: {PermRead, PermWrite, PermDelete},
|
||||
ResAudit: {PermRead, PermWrite, PermDelete},
|
||||
},
|
||||
"manager": {
|
||||
ResCustomers: {PermRead, PermWrite},
|
||||
ResEmployees: {PermRead, PermWrite},
|
||||
ResFinance: {PermRead},
|
||||
ResLegal: {PermRead},
|
||||
ResHR: {PermRead, PermWrite},
|
||||
},
|
||||
"user": {
|
||||
ResCustomers: {PermRead},
|
||||
ResEmployees: {PermRead},
|
||||
ResFinance: {PermRead},
|
||||
},
|
||||
"viewer": {
|
||||
ResCustomers: {PermRead},
|
||||
ResEmployees: {PermRead},
|
||||
},
|
||||
}
|
||||
|
||||
// HasPermission kontrollerar om en roll har en specifik permission
|
||||
func HasPermission(role string, resource Resource, permission Permission) bool {
|
||||
perms, ok := RolePermissions[role]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
resourcePerms, ok := perms[resource]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, p := range resourcePerms {
|
||||
if p == permission || p == PermAdmin {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RBACMiddleware kontrollerar behörigheter
|
||||
func RBACMiddleware(resource Resource, permission Permission) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Hämta roll från context (satt av auth middleware)
|
||||
role, ok := r.Context().Value("role").(string)
|
||||
if !ok {
|
||||
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if !HasPermission(role, resource, permission) {
|
||||
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── Rate Limiting ─────────────────────────────────────────────────────────
|
||||
|
||||
type RateLimiter struct {
|
||||
limiters map[string]*rate.Limiter
|
||||
}
|
||||
|
||||
func NewRateLimiter() *RateLimiter {
|
||||
return &RateLimiter{
|
||||
limiters: make(map[string]*rate.Limiter),
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *RateLimiter) GetLimiter(key string) *rate.Limiter {
|
||||
limiter, ok := rl.limiters[key]
|
||||
if !ok {
|
||||
limiter = rate.NewLimiter(rate.Every(time.Second), 10) // 10 req/s
|
||||
rl.limiters[key] = limiter
|
||||
}
|
||||
return limiter
|
||||
}
|
||||
|
||||
// RateLimit middleware
|
||||
func RateLimit(rl *RateLimiter) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
key := r.RemoteAddr
|
||||
if !rl.GetLimiter(key).Allow() {
|
||||
http.Error(w, `{"error":"rate limit exceeded"}`, http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── Audit Log ─────────────────────────────────────────────────────────────
|
||||
|
||||
type AuditEvent struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
UserID string `json:"user_id"`
|
||||
Action string `json:"action"`
|
||||
Resource string `json:"resource"`
|
||||
ResourceID string `json:"resource_id,omitempty"`
|
||||
IP string `json:"ip"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Success bool `json:"success"`
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// AuditLog middleware loggar alla requests
|
||||
func AuditLog(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
// Wrap response writer för att fånga status code
|
||||
wrapped := &responseRecorder{ResponseWriter: w, statusCode: http.StatusOK}
|
||||
|
||||
next.ServeHTTP(wrapped, r)
|
||||
|
||||
// Logga audit event
|
||||
event := AuditEvent{
|
||||
Timestamp: start,
|
||||
Action: r.Method,
|
||||
Resource: r.URL.Path,
|
||||
IP: r.RemoteAddr,
|
||||
UserAgent: r.UserAgent(),
|
||||
Success: wrapped.statusCode < 400,
|
||||
}
|
||||
|
||||
// Hämta user ID från context om finns
|
||||
if userID, ok := r.Context().Value("user_id").(string); ok {
|
||||
event.UserID = userID
|
||||
}
|
||||
|
||||
// TODO: Spara till databas eller skicka till Kafka
|
||||
_ = event
|
||||
})
|
||||
}
|
||||
|
||||
type responseRecorder struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (rr *responseRecorder) WriteHeader(code int) {
|
||||
rr.statusCode = code
|
||||
rr.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
// ── Security Headers ──────────────────────────────────────────────────────
|
||||
|
||||
func SecurityHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
||||
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'self'")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user