feat: integrate Grafana dashboards into BOC DashboardPage
- 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
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package sms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// RedisStore implementerar VerificationStore med Redis
|
||||
type RedisStore struct {
|
||||
client *redis.Client
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewRedisStore skapar ny Redis-baserad store
|
||||
func NewRedisStore(addr string) *RedisStore {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: addr,
|
||||
Password: "",
|
||||
DB: 0,
|
||||
})
|
||||
|
||||
return &RedisStore{
|
||||
client: client,
|
||||
ctx: context.Background(),
|
||||
}
|
||||
}
|
||||
|
||||
// Set sparar värde med TTL
|
||||
func (r *RedisStore) Set(key string, value string, ttl time.Duration) error {
|
||||
return r.client.Set(r.ctx, key, value, ttl).Err()
|
||||
}
|
||||
|
||||
// Get hämtar värde
|
||||
func (r *RedisStore) Get(key string) (string, error) {
|
||||
val, err := r.client.Get(r.ctx, key).Result()
|
||||
if err == redis.Nil {
|
||||
return "", fmt.Errorf("key not found")
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// Delete raderar nyckel
|
||||
func (r *RedisStore) Delete(key string) error {
|
||||
return r.client.Del(r.ctx, key).Err()
|
||||
}
|
||||
|
||||
// Ping kontrollerar anslutning
|
||||
func (r *RedisStore) Ping() error {
|
||||
return r.client.Ping(r.ctx).Err()
|
||||
}
|
||||
Reference in New Issue
Block a user