e5623d2f84
- Add Infrastructure Health section with CPU/Memory/Disk panels - Add Service Status section with PM2/Docker panels - Create GrafanaPanel component for iframe embedding - Build passes successfully
286 lines
7.8 KiB
Go
286 lines
7.8 KiB
Go
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
|
|
}
|