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
This commit is contained in:
@@ -0,0 +1,425 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user