Files
boc/backend/handlers/visma.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

294 lines
7.4 KiB
Go

package handlers
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
// VismaHandler hanterar riktig Visma-integration
type VismaHandler struct {
clientID string
clientSecret string
redirectURI string
accessToken string
refreshToken string
tokenExpiry time.Time
client *http.Client
}
// NewVismaHandler skapar en ny handler
func NewVismaHandler() *VismaHandler {
return &VismaHandler{
clientID: os.Getenv("VISMA_CLIENT_ID"),
clientSecret: os.Getenv("VISMA_CLIENT_SECRET"),
redirectURI: os.Getenv("VISMA_REDIRECT_URI"),
client: &http.Client{Timeout: 30 * time.Second},
}
}
// IsConfigured returnerar true om Visma är konfigurerat
func (h *VismaHandler) IsConfigured() bool {
return h.clientID != "" && h.clientSecret != ""
}
// VismaCompany representerar ett Visma-företag
type VismaCompany struct {
ID string `json:"id"`
Name string `json:"name"`
OrgNumber string `json:"organisationNumber"`
}
// VismaVoucher representerar ett Visma-verifikat
type VismaVoucher struct {
ID string `json:"id"`
VoucherDate string `json:"voucherDate"`
Text string `json:"text"`
Rows []VismaRow `json:"rows"`
Modified time.Time `json:"modifiedUtc"`
}
// VismaRow representerar en verifikatrad
type VismaRow struct {
AccountID string `json:"accountId"`
AccountName string `json:"accountName"`
DebitAmount float64 `json:"debitAmount"`
CreditAmount float64 `json:"creditAmount"`
Description string `json:"description"`
}
// GetAuthURL returnerar Visma OAuth URL
func (h *VismaHandler) GetAuthURL(w http.ResponseWriter, r *http.Request) {
if !h.IsConfigured() {
writeJSON(w, http.StatusServiceUnavailable, map[string]interface{}{
"ok": false,
"error": "Visma not configured",
})
return
}
authURL := fmt.Sprintf(
"https://eaccountingapi.vismaonline.com/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&scope=ea:api",
h.clientID,
h.redirectURI,
)
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": true,
"auth_url": authURL,
})
}
// HandleCallback hanterar Visma OAuth callback
func (h *VismaHandler) HandleCallback(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
if code == "" {
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
"ok": false,
"error": "missing code",
})
return
}
// Byt kod mot token
tokenURL := "https://eaccountingapi.vismaonline.com/oauth/token"
reqBody := fmt.Sprintf("grant_type=authorization_code&code=%s&redirect_uri=%s&client_id=%s&client_secret=%s",
code, h.redirectURI, h.clientID, h.clientSecret)
req, err := http.NewRequest("POST", tokenURL, strings.NewReader(reqBody))
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
"ok": false,
"error": err.Error(),
})
return
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := h.client.Do(req)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
"ok": false,
"error": err.Error(),
})
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
"ok": false,
"error": err.Error(),
})
return
}
var tokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
}
if err := json.Unmarshal(body, &tokenResp); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
"ok": false,
"error": err.Error(),
})
return
}
h.accessToken = tokenResp.AccessToken
h.refreshToken = tokenResp.RefreshToken
h.tokenExpiry = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": true,
"authenticated": true,
"expires": h.tokenExpiry,
})
}
// GetCompanies hämtar företag från Visma
func (h *VismaHandler) GetCompanies(w http.ResponseWriter, r *http.Request) {
if h.accessToken == "" {
writeJSON(w, http.StatusUnauthorized, map[string]interface{}{
"ok": false,
"error": "not authenticated",
})
return
}
req, err := http.NewRequest("GET", "https://eaccountingapi.vismaonline.com/v2/companysettings", nil)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
"ok": false,
"error": err.Error(),
})
return
}
req.Header.Set("Authorization", "Bearer "+h.accessToken)
req.Header.Set("Accept", "application/json")
resp, err := h.client.Do(req)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
"ok": false,
"error": err.Error(),
})
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
"ok": false,
"error": err.Error(),
})
return
}
if resp.StatusCode != http.StatusOK {
writeJSON(w, resp.StatusCode, map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("Visma API error: %s", string(body)),
"status": resp.StatusCode,
})
return
}
var companies []VismaCompany
if err := json.Unmarshal(body, &companies); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
"ok": false,
"error": err.Error(),
})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": true,
"companies": companies,
})
}
// GetVouchers hämtar verifikat från Visma
func (h *VismaHandler) GetVouchers(w http.ResponseWriter, r *http.Request) {
if h.accessToken == "" {
writeJSON(w, http.StatusUnauthorized, map[string]interface{}{
"ok": false,
"error": "not authenticated",
})
return
}
// Hämta vouchers från Visma API
req, err := http.NewRequest("GET", "https://eaccountingapi.vismaonline.com/v2/vouchers", nil)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
"ok": false,
"error": err.Error(),
})
return
}
req.Header.Set("Authorization", "Bearer "+h.accessToken)
req.Header.Set("Accept", "application/json")
resp, err := h.client.Do(req)
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("Visma API error: %v", err),
})
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
writeJSON(w, resp.StatusCode, map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("Visma API returned %d: %s", resp.StatusCode, string(body)),
})
return
}
var vouchers []VismaVoucher
if err := json.NewDecoder(resp.Body).Decode(&vouchers); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
"ok": false,
"error": "Failed to decode Visma response",
})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": true,
"vouchers": vouchers,
"source": "visma",
})
}
// GetStatus returnerar Visma-kopplingsstatus
func (h *VismaHandler) GetStatus(w http.ResponseWriter, r *http.Request) {
status := map[string]interface{}{
"configured": h.IsConfigured(),
"authenticated": h.accessToken != "",
"client_id": h.clientID,
"token_expiry": h.tokenExpiry,
}
if !h.IsConfigured() {
status["setup_url"] = "/api/v1/visma/auth"
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": true,
"visma": status,
})
}