78b57273e2
- 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
426 lines
13 KiB
Go
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
|
|
}
|