package sms import ( "bytes" "encoding/json" "fmt" "net/http" "os" "time" ) // ELK46Config konfiguration för 46elks API type ELK46Config struct { APIUsername string APIPassword string BaseURL string FromNumber string // Avsändarnummer (t.ex. "Landvex") DryRun bool // Testläge } // DefaultConfig skapar konfig från miljövariabler func DefaultConfig() *ELK46Config { return &ELK46Config{ APIUsername: getEnv("ELK46_USERNAME", ""), APIPassword: getEnv("ELK46_PASSWORD", ""), BaseURL: getEnv("ELK46_BASE_URL", "https://api.46elks.com/a1"), FromNumber: getEnv("ELK46_FROM", "Landvex"), DryRun: getEnv("ELK46_DRY_RUN", "true") == "true", } } func getEnv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } // ELK46Client klient för 46elks API type ELK46Client struct { config *ELK46Config client *http.Client } // NewClient skapar ny klient func NewClient(config *ELK46Config) *ELK46Client { if config == nil { config = DefaultConfig() } return &ELK46Client{ config: config, client: &http.Client{Timeout: 30 * time.Second}, } } // IsConfigured kontrollerar om API-nycklar finns func (c *ELK46Client) IsConfigured() bool { return c.config.APIUsername != "" && c.config.APIPassword != "" } // SendSMSRequest begäran att skicka SMS type SendSMSRequest struct { To string `json:"to"` // Telefonnummer i E.164-format (+46701234567) From string `json:"from"` // Avsändare (nummer eller text, max 11 tecken) Message string `json:"message"` // Meddelande (max 1600 tecken) DryRun bool `json:"dryrun,omitempty"` } // SendSMSResponse svar från API type SendSMSResponse struct { ID string `json:"id"` To string `json:"to"` From string `json:"from"` Message string `json:"message"` Cost int `json:"cost"` Currency string `json:"currency"` Status string `json:"status"` } // SendSMS skickar ett SMS func (c *ELK46Client) SendSMS(to, message string) (*SendSMSResponse, error) { if !c.IsConfigured() { return nil, fmt.Errorf("ELK46 not configured: missing API credentials") } req := SendSMSRequest{ To: formatPhoneNumber(to), From: c.config.FromNumber, Message: message, DryRun: c.config.DryRun, } body, _ := json.Marshal(req) httpReq, err := http.NewRequest("POST", c.config.BaseURL+"/SMS", bytes.NewBuffer(body)) if err != nil { return nil, err } httpReq.Header.Set("Content-Type", "application/json") httpReq.SetBasicAuth(c.config.APIUsername, c.config.APIPassword) resp, err := c.client.Do(httpReq) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("ELK46 API error: %d", resp.StatusCode) } var result SendSMSResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } return &result, nil } // SendBulkSMS skickar SMS till flera mottagare func (c *ELK46Client) SendBulkSMS(recipients []string, message string) ([]*SendSMSResponse, error) { var results []*SendSMSResponse for _, to := range recipients { resp, err := c.SendSMS(to, message) if err != nil { return results, err } results = append(results, resp) } return results, nil } // formatPhoneNumber formaterar svenskt nummer till E.164 func formatPhoneNumber(phone string) string { // Ta bort alla icke-siffror var digits string for _, r := range phone { if r >= '0' && r <= '9' { digits += string(r) } } // Om börjar med 07, lägg till +46 if len(digits) == 10 && digits[0] == '0' { return "+46" + digits[1:] } // Om redan har landskod if len(digits) > 10 { return "+" + digits } return phone } // ========================================== // TVÅVÄGSVERIFIERING (2FA) // ========================================== // VerificationStore lagrar aktiva verifieringskoder type VerificationStore interface { Set(key string, code string, ttl time.Duration) error Get(key string) (string, error) Delete(key string) error } // TwoFactorAuth hanterar tvåvägsverifiering via SMS type TwoFactorAuth struct { client *ELK46Client store VerificationStore } // NewTwoFactorAuth skapar ny 2FA-hanterare func NewTwoFactorAuth(client *ELK46Client, store VerificationStore) *TwoFactorAuth { return &TwoFactorAuth{ client: client, store: store, } } // GenerateCode genererar en 6-siffrig kod func (tfa *TwoFactorAuth) GenerateCode() string { // Enkel implementation — använd crypto/rand i produktion return fmt.Sprintf("%06d", time.Now().UnixNano()%1000000) } // SendVerificationCode skickar verifieringskod via SMS func (tfa *TwoFactorAuth) SendVerificationCode(phone string) (string, error) { if !tfa.client.IsConfigured() { return "", fmt.Errorf("SMS not configured") } code := tfa.GenerateCode() key := "2fa:" + formatPhoneNumber(phone) // Spara kod i store (Redis) med 10 minuters TTL if err := tfa.store.Set(key, code, 10*time.Minute); err != nil { return "", err } message := fmt.Sprintf("Din verifieringskod för Landvex är: %s. Gäller i 10 minuter.", code) _, err := tfa.client.SendSMS(phone, message) if err != nil { return "", err } return code, nil } // VerifyCode verifierar en kod func (tfa *TwoFactorAuth) VerifyCode(phone, code string) (bool, error) { key := "2fa:" + formatPhoneNumber(phone) storedCode, err := tfa.store.Get(key) if err != nil { return false, err } if storedCode != code { return false, nil } // Radera kod efter användning tfa.store.Delete(key) return true, nil } // ========================================== // NOTIFICATIONS // ========================================== // NotificationService skickar notifikationer via SMS type NotificationService struct { client *ELK46Client } // NewNotificationService skapar ny notifikationstjänst func NewNotificationService(client *ELK46Client) *NotificationService { return &NotificationService{client: client} } // SendTaskReminder skickar påminnelse om uppgift func (n *NotificationService) SendTaskReminder(phone, taskName, dueDate string) error { message := fmt.Sprintf("Påminnelse: '%s' ska vara klar %s. Logga in på BOC för detaljer.", taskName, dueDate) _, err := n.client.SendSMS(phone, message) return err } // SendOnboardingWelcome skickar välkomst-SMS till ny medarbetare func (n *NotificationService) SendOnboardingWelcome(phone, name string) error { message := fmt.Sprintf("Välkommen till Landvex, %s! Din onboarding har startats. Logga in på boc.aamos.systems för att komma igång.", name) _, err := n.client.SendSMS(phone, message) return err } // SendDocumentSignatureRequest skickar begäran om signering func (n *NotificationService) SendDocumentSignatureRequest(phone, documentName string) error { message := fmt.Sprintf("Du har ett dokument att signera: '%s'. Logga in på BOC för att signera.", documentName) _, err := n.client.SendSMS(phone, message) return err } // SendPerformanceReviewReminder skickar påminnelse om medarbetarsamtal func (n *NotificationService) SendPerformanceReviewReminder(phone, reviewerName string) error { message := fmt.Sprintf("Påminnelse: Medarbetarsamtal med %s är inplanerat. Förbered dig via BOC.", reviewerName) _, err := n.client.SendSMS(phone, message) return err } // SendTrainingExpiryWarning varnar om utgående certifiering func (n *NotificationService) SendTrainingExpiryWarning(phone, trainingName string, daysLeft int) error { message := fmt.Sprintf("Din certifiering '%s' går ut om %d dagar. Förnya via BOC.", trainingName, daysLeft) _, err := n.client.SendSMS(phone, message) return err } // SendSecurityAlert skickar säkerhetsvarning func (n *NotificationService) SendSecurityAlert(phone, alertType string) error { message := fmt.Sprintf("SÄKERHETSVARNING: %s upptäckt på ditt konto. Kontakta IT omedelbart om detta inte var du.", alertType) _, err := n.client.SendSMS(phone, message) return err }