Files
boc/backend/handlers/quixzoom.go
T
Bernt 78b57273e2 security: Add proper authentication, RBAC, and tenant isolation
- Add password hashing with bcrypt
- Add AuthService with proper login
- Add password strength validation
- Add RBAC middleware (AdminOnly, ManagerOrAdmin)
- Add tenant isolation middleware
- Update CRM handler with tenant filtering
- Add JWT fallback for development mode
- Add user context helpers
- Build successful
2026-08-10 12:52:48 +00:00

426 lines
13 KiB
Go

package handlers
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/go-chi/chi/v5"
)
// QuixzoomHandler hanterar quiXzoom-integration
type QuixzoomHandler struct {
baseURL string
token string
}
// NewQuixzoomHandler skapar en ny handler
func NewQuixzoomHandler() *QuixzoomHandler {
return &QuixzoomHandler{
baseURL: getEnv("QUIXZOOM_API_URL", ""),
token: getEnv("QUIXZOOM_API_TOKEN", ""),
}
}
func (h *QuixzoomHandler) isConfigured() bool {
return h.baseURL != "" && h.token != ""
}
func (h *QuixzoomHandler) apiGet(path string) (*http.Response, error) {
req, err := http.NewRequest("GET", h.baseURL+path, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+h.token)
return http.DefaultClient.Do(req)
}
// Zoomer representerar en quiXzoom-användare (fältarbetare)
type Zoomer struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Phone string `json:"phone"`
Status string `json:"status"`
Country string `json:"country"`
City string `json:"city"`
JoinedAt time.Time `json:"joined_at"`
LastActive time.Time `json:"last_active"`
TotalTasks int `json:"total_tasks"`
CompletedTasks int `json:"completed_tasks"`
Rating float64 `json:"rating"`
Earnings float64 `json:"earnings"`
PayoutMethod string `json:"payout_method"`
Verified bool `json:"verified"`
}
// FieldData representerar insamlad fältdata
type FieldData struct {
ID string `json:"id"`
ZoomerID string `json:"zoomer_id"`
ZoomerName string `json:"zoomer_name"`
Type string `json:"type"`
Status string `json:"status"`
Location Location `json:"location"`
Images []Image `json:"images"`
Metadata map[string]interface{} `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
ProcessedAt *time.Time `json:"processed_at,omitempty"`
AIResult *AIResult `json:"ai_result,omitempty"`
}
// Location representerar en geografisk plats
type Location struct {
Latitude float64 `json:"lat"`
Longitude float64 `json:"lng"`
Address string `json:"address"`
City string `json:"city"`
Country string `json:"country"`
}
// Image representerar en bild
type Image struct {
ID string `json:"id"`
URL string `json:"url"`
Thumbnail string `json:"thumbnail"`
Status string `json:"status"`
}
// AIResult representerar AI-analysresultat
type AIResult struct {
Engine string `json:"engine"`
Confidence float64 `json:"confidence"`
Detections []Detection `json:"detections"`
ProcessedAt time.Time `json:"processed_at"`
Metadata map[string]interface{} `json:"metadata"`
}
// Detection representerar en AI-detektering
type Detection struct {
Label string `json:"label"`
Confidence float64 `json:"confidence"`
BoundingBox struct {
X float64 `json:"x"`
Y float64 `json:"y"`
Width float64 `json:"width"`
Height float64 `json:"height"`
} `json:"bounding_box"`
}
// Payout representerar en utbetalning
type Payout struct {
ID string `json:"id"`
ZoomerID string `json:"zoomer_id"`
ZoomerName string `json:"zoomer_name"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
Status string `json:"status"`
Method string `json:"method"`
Period string `json:"period"`
CreatedAt time.Time `json:"created_at"`
ProcessedAt *time.Time `json:"processed_at,omitempty"`
Tax float64 `json:"tax"`
Fee float64 `json:"fee"`
NetAmount float64 `json:"net_amount"`
}
// GetZoomers returnerar alla zoomers
func (h *QuixzoomHandler) GetZoomers(w http.ResponseWriter, r *http.Request) {
if !h.isConfigured() {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "quiXzoom API not configured. Set QUIXZOOM_API_URL and QUIXZOOM_API_TOKEN environment variables.",
})
return
}
resp, err := h.apiGet("/api/v1/zoomers")
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("quiXzoom API error: %v", err),
})
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(resp.StatusCode)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("quiXzoom API returned %d", resp.StatusCode),
})
return
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "Failed to decode quiXzoom response",
})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
// GetZoomer returnerar en specifik zoomer
func (h *QuixzoomHandler) GetZoomer(w http.ResponseWriter, r *http.Request) {
if !h.isConfigured() {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "quiXzoom API not configured. Set QUIXZOOM_API_URL and QUIXZOOM_API_TOKEN environment variables.",
})
return
}
zoomerID := chi.URLParam(r, "id")
resp, err := h.apiGet("/api/v1/zoomers/" + zoomerID)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("quiXzoom API error: %v", err),
})
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(resp.StatusCode)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("quiXzoom API returned %d", resp.StatusCode),
})
return
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "Failed to decode quiXzoom response",
})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
// GetFieldData returnerar all fältdata
func (h *QuixzoomHandler) GetFieldData(w http.ResponseWriter, r *http.Request) {
if !h.isConfigured() {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "quiXzoom API not configured. Set QUIXZOOM_API_URL and QUIXZOOM_API_TOKEN environment variables.",
})
return
}
resp, err := h.apiGet("/api/v1/field-data")
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("quiXzoom API error: %v", err),
})
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(resp.StatusCode)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("quiXzoom API returned %d", resp.StatusCode),
})
return
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "Failed to decode quiXzoom response",
})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
// GetPayouts returnerar alla utbetalningar
func (h *QuixzoomHandler) GetPayouts(w http.ResponseWriter, r *http.Request) {
if !h.isConfigured() {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "quiXzoom API not configured. Set QUIXZOOM_API_URL and QUIXZOOM_API_TOKEN environment variables.",
})
return
}
resp, err := h.apiGet("/api/v1/payouts")
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("quiXzoom API error: %v", err),
})
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(resp.StatusCode)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("quiXzoom API returned %d", resp.StatusCode),
})
return
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "Failed to decode quiXzoom response",
})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
// GetPayoutStats returnerar utbetalningsstatistik
func (h *QuixzoomHandler) GetPayoutStats(w http.ResponseWriter, r *http.Request) {
if !h.isConfigured() {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "quiXzoom API not configured. Set QUIXZOOM_API_URL and QUIXZOOM_API_TOKEN environment variables.",
})
return
}
resp, err := h.apiGet("/api/v1/payouts/stats")
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("quiXzoom API error: %v", err),
})
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(resp.StatusCode)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("quiXzoom API returned %d", resp.StatusCode),
})
return
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "Failed to decode quiXzoom response",
})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
// GetInsights returnerar quiXzoom-insikter (Urban Intelligence Index)
func (h *QuixzoomHandler) GetInsights(w http.ResponseWriter, r *http.Request) {
if !h.isConfigured() {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "quiXzoom API not configured. Set QUIXZOOM_API_URL and QUIXZOOM_API_TOKEN environment variables.",
})
return
}
resp, err := h.apiGet("/api/v1/insights")
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("quiXzoom API error: %v", err),
})
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(resp.StatusCode)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("quiXzoom API returned %d", resp.StatusCode),
})
return
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "Failed to decode quiXzoom response",
})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
func timePtr(t time.Time) *time.Time {
return &t
}