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,298 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SIE4Handler hanterar SIE4-import/export
|
||||
type SIE4Handler struct{}
|
||||
|
||||
func NewSIE4Handler() *SIE4Handler {
|
||||
return &SIE4Handler{}
|
||||
}
|
||||
|
||||
// SIE4Entry representerar en SIE4-post
|
||||
type SIE4Entry struct {
|
||||
Date string `json:"date"`
|
||||
VoucherNo string `json:"voucher_no"`
|
||||
Account string `json:"account"`
|
||||
Description string `json:"description"`
|
||||
Amount float64 `json:"amount"`
|
||||
Dimension string `json:"dimension,omitempty"`
|
||||
}
|
||||
|
||||
// ParseSIE4 parsar SIE4-fil
|
||||
func (h *SIE4Handler) ParseSIE4(w http.ResponseWriter, r *http.Request) {
|
||||
var reader io.Reader
|
||||
|
||||
// Kolla om det är multipart (filuppladdning) eller JSON med content
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "multipart/form-data") {
|
||||
// Läs uppladdad fil
|
||||
file, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
|
||||
"ok": false,
|
||||
"error": "missing file",
|
||||
})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
reader = file
|
||||
} else {
|
||||
// Läs JSON med content
|
||||
var req struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
|
||||
"ok": false,
|
||||
"error": "invalid request: expected multipart file or JSON with content field",
|
||||
})
|
||||
return
|
||||
}
|
||||
reader = strings.NewReader(req.Content)
|
||||
}
|
||||
|
||||
entries, parseErr := h.parseSIE4File(reader)
|
||||
if parseErr != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
|
||||
"ok": false,
|
||||
"error": parseErr.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"ok": true,
|
||||
"entries": entries,
|
||||
"count": len(entries),
|
||||
})
|
||||
}
|
||||
|
||||
// GenerateSIE4 genererar SIE4-fil
|
||||
func (h *SIE4Handler) GenerateSIE4(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Company string `json:"company"`
|
||||
OrgNumber string `json:"org_number"`
|
||||
FiscalYear string `json:"fiscal_year"`
|
||||
Entries []SIE4Entry `json:"entries"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
|
||||
"ok": false,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
sie4Content := h.generateSIE4Content(req.Company, req.OrgNumber, req.FiscalYear, req.Entries)
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=ISO-8859-1")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s_%s.SI", req.OrgNumber, req.FiscalYear))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(sie4Content))
|
||||
}
|
||||
|
||||
// parseSIE4File parsar en SIE4-fil
|
||||
func (h *SIE4Handler) parseSIE4File(reader io.Reader) ([]SIE4Entry, error) {
|
||||
var entries []SIE4Entry
|
||||
scanner := bufio.NewScanner(reader)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Hoppa över tomma rader och kommentarer
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parsa #VER (verifikat)
|
||||
if strings.HasPrefix(line, "#VER") {
|
||||
// Format: #VER "serie" "voucherno" "date" "description" "date_created"
|
||||
parts := h.parseSIELine(line)
|
||||
if len(parts) >= 4 {
|
||||
voucherNo := h.unquote(parts[2])
|
||||
date := h.unquote(parts[3])
|
||||
description := ""
|
||||
if len(parts) >= 5 {
|
||||
description = h.unquote(parts[4])
|
||||
}
|
||||
|
||||
// Läs tillhörande rader
|
||||
for scanner.Scan() {
|
||||
rowLine := scanner.Text()
|
||||
rowLine = strings.TrimSpace(rowLine)
|
||||
|
||||
if rowLine == "}" {
|
||||
break
|
||||
}
|
||||
|
||||
if strings.HasPrefix(rowLine, "#TRANS") {
|
||||
// Format: #TRANS account { amount "date" "description" }
|
||||
rowParts := h.parseSIELine(rowLine)
|
||||
if len(rowParts) >= 4 {
|
||||
account := h.unquote(rowParts[1])
|
||||
amountStr := rowParts[3]
|
||||
amount, _ := strconv.ParseFloat(amountStr, 64)
|
||||
|
||||
rowDesc := description
|
||||
if len(rowParts) >= 6 {
|
||||
rowDesc = h.unquote(rowParts[5])
|
||||
}
|
||||
|
||||
entries = append(entries, SIE4Entry{
|
||||
Date: date,
|
||||
VoucherNo: voucherNo,
|
||||
Account: account,
|
||||
Description: rowDesc,
|
||||
Amount: amount,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// generateSIE4Content genererar SIE4-innehåll
|
||||
func (h *SIE4Handler) generateSIE4Content(company, orgNumber, fiscalYear string, entries []SIE4Entry) string {
|
||||
var sb strings.Builder
|
||||
|
||||
// SIE4 header
|
||||
sb.WriteString("#FLAGGA 0\n")
|
||||
sb.WriteString(fmt.Sprintf("#FORMAT PC8\n"))
|
||||
sb.WriteString(fmt.Sprintf("#SIETYP 4\n"))
|
||||
sb.WriteString("#PROGRAM \"AMOS BOC\" 1.0\n")
|
||||
sb.WriteString(fmt.Sprintf("#GEN %s\n", time.Now().Format("20060102")))
|
||||
sb.WriteString(fmt.Sprintf("#FNAMN \"%s\"\n", company))
|
||||
sb.WriteString(fmt.Sprintf("#ORGNR \"%s\"\n", orgNumber))
|
||||
sb.WriteString(fmt.Sprintf("#RAR 0 %s0101 %s1231\n", fiscalYear, fiscalYear))
|
||||
sb.WriteString("#KPTYP EUBAS97\n")
|
||||
|
||||
// Kontoplan (BAS-konton)
|
||||
accounts := h.getBASAccounts()
|
||||
for code, name := range accounts {
|
||||
sb.WriteString(fmt.Sprintf("#KONTO %s \"%s\"\n", code, name))
|
||||
}
|
||||
|
||||
// Verifikat
|
||||
vouchers := h.groupByVoucher(entries)
|
||||
for voucherNo, voucherEntries := range vouchers {
|
||||
if len(voucherEntries) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
first := voucherEntries[0]
|
||||
sb.WriteString(fmt.Sprintf("#VER \"\" \"%s\" %s \"%s\" %s\n",
|
||||
voucherNo,
|
||||
h.formatSIEDate(first.Date),
|
||||
first.Description,
|
||||
time.Now().Format("20060102")))
|
||||
sb.WriteString("{\n")
|
||||
|
||||
for _, entry := range voucherEntries {
|
||||
sb.WriteString(fmt.Sprintf("#TRANS %s {} %s \"%s\" \"%s\"\n",
|
||||
entry.Account,
|
||||
h.formatSIEAmount(entry.Amount),
|
||||
h.formatSIEDate(entry.Date),
|
||||
entry.Description))
|
||||
}
|
||||
|
||||
sb.WriteString("}\n")
|
||||
}
|
||||
|
||||
// Slut
|
||||
sb.WriteString("#SLUT\n")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Helper functions för SIE4
|
||||
|
||||
func (h *SIE4Handler) parseSIELine(line string) []string {
|
||||
var parts []string
|
||||
var current strings.Builder
|
||||
inQuotes := false
|
||||
|
||||
for _, ch := range line {
|
||||
switch ch {
|
||||
case '"':
|
||||
if inQuotes {
|
||||
parts = append(parts, current.String())
|
||||
current.Reset()
|
||||
}
|
||||
inQuotes = !inQuotes
|
||||
case ' ', '\t':
|
||||
if !inQuotes && current.Len() > 0 {
|
||||
parts = append(parts, current.String())
|
||||
current.Reset()
|
||||
} else if inQuotes {
|
||||
current.WriteRune(ch)
|
||||
}
|
||||
default:
|
||||
current.WriteRune(ch)
|
||||
}
|
||||
}
|
||||
|
||||
if current.Len() > 0 {
|
||||
parts = append(parts, current.String())
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
func (h *SIE4Handler) unquote(s string) string {
|
||||
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
|
||||
return s[1 : len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (h *SIE4Handler) formatSIEDate(date string) string {
|
||||
// Konvertera YYYY-MM-DD till YYYYMMDD
|
||||
return strings.ReplaceAll(date, "-", "")
|
||||
}
|
||||
|
||||
func (h *SIE4Handler) formatSIEAmount(amount float64) string {
|
||||
// SIE4 använder punkt som decimaltecken
|
||||
return fmt.Sprintf("%.2f", amount)
|
||||
}
|
||||
|
||||
func (h *SIE4Handler) groupByVoucher(entries []SIE4Entry) map[string][]SIE4Entry {
|
||||
groups := make(map[string][]SIE4Entry)
|
||||
for _, entry := range entries {
|
||||
groups[entry.VoucherNo] = append(groups[entry.VoucherNo], entry)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
func (h *SIE4Handler) getBASAccounts() map[string]string {
|
||||
return map[string]string{
|
||||
"1510": "Kundfordringar",
|
||||
"1930": "Företagskonto",
|
||||
"2010": "Eget kapital",
|
||||
"2440": "Leverantörsskulder",
|
||||
"2610": "Utgående moms",
|
||||
"3010": "Försäljning tjänster",
|
||||
"6540": "IT-kostnader",
|
||||
"7210": "Löner",
|
||||
"7690": "Övriga personalkostnader",
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user