Files
boc/backend/sms/elk46.go
T

286 lines
7.8 KiB
Go
Raw Normal View History

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
}