| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586 |
- package store
- import (
- "bytes"
- "context"
- "database/sql"
- "encoding/json"
- "errors"
- "fmt"
- "sort"
- "strings"
- "time"
- )
- func DefaultNotificationSensitiveFields(channel string) []string {
- switch strings.ToLower(strings.TrimSpace(channel)) {
- case "telegram":
- return []string{"bot_token"}
- case "email":
- return []string{"password"}
- case "webhook":
- return []string{"secret"}
- case "pushplus":
- return []string{"token"}
- default:
- return nil
- }
- }
- func (s *Store) UpsertNotificationSetting(
- ctx context.Context,
- value NotificationSetting,
- ) error {
- tx, err := s.db.BeginTx(ctx, nil)
- if err != nil {
- return fmt.Errorf("begin notification setting update: %w", err)
- }
- defer tx.Rollback()
- if err := upsertNotificationSetting(ctx, tx, value); err != nil {
- return err
- }
- if err := tx.Commit(); err != nil {
- return fmt.Errorf("commit notification setting update: %w", err)
- }
- return nil
- }
- func upsertNotificationSetting(
- ctx context.Context,
- executor contextQueryExecer,
- value NotificationSetting,
- ) error {
- value.Channel = strings.ToLower(strings.TrimSpace(value.Channel))
- if value.Channel == "" {
- return errors.New("notification channel is required")
- }
- config, err := normalizeJSONObject(value.Config)
- if err != nil {
- return fmt.Errorf("normalize %s notification config: %w", value.Channel, err)
- }
- current, currentErr := notificationSetting(executor.QueryRowContext(
- ctx,
- notificationSettingSelect+` WHERE channel = ?`,
- value.Channel,
- ))
- if currentErr != nil && !errors.Is(currentErr, ErrNotFound) {
- return fmt.Errorf("read %s notification setting before update: %w", value.Channel, currentErr)
- }
- fields := uniqueNonemptyStrings(
- DefaultNotificationSensitiveFields(value.Channel),
- value.SensitiveFields,
- )
- if currentErr == nil {
- fields = uniqueNonemptyStrings(fields, current.SensitiveFields)
- config, err = mergeJSONSecrets(config, current.Config, fields)
- if err != nil {
- return fmt.Errorf("preserve %s notification secrets: %w", value.Channel, err)
- }
- }
- fieldsJSON, err := json.Marshal(fields)
- if err != nil {
- return fmt.Errorf("encode notification sensitive fields: %w", err)
- }
- now := time.Now().UTC()
- createdAt := value.CreatedAt
- if createdAt.IsZero() {
- createdAt = now
- }
- updatedAt := value.UpdatedAt
- if updatedAt.IsZero() {
- updatedAt = now
- }
- _, err = executor.ExecContext(ctx, `
- INSERT INTO notification_settings (
- channel, enabled, config_json, sensitive_fields_json,
- created_at, updated_at
- ) VALUES (?, ?, ?, ?, ?, ?)
- ON CONFLICT(channel) DO UPDATE SET
- enabled = excluded.enabled,
- config_json = excluded.config_json,
- sensitive_fields_json = excluded.sensitive_fields_json,
- updated_at = excluded.updated_at
- `,
- value.Channel, boolInt(value.Enabled), string(config),
- string(fieldsJSON), createdAt.Unix(), updatedAt.Unix(),
- )
- if err != nil {
- return fmt.Errorf("upsert %s notification setting: %w", value.Channel, err)
- }
- return nil
- }
- // SaveNotificationSettings applies a multi-channel settings form atomically.
- func (s *Store) SaveNotificationSettings(
- ctx context.Context,
- values []NotificationSetting,
- ) error {
- tx, err := s.db.BeginTx(ctx, nil)
- if err != nil {
- return fmt.Errorf("begin notification settings batch: %w", err)
- }
- defer tx.Rollback()
- seen := make(map[string]struct{}, len(values))
- for index, value := range values {
- channel := strings.ToLower(strings.TrimSpace(value.Channel))
- if _, duplicate := seen[channel]; duplicate {
- return fmt.Errorf("duplicate notification channel %q", channel)
- }
- if err := upsertNotificationSetting(ctx, tx, value); err != nil {
- return fmt.Errorf("save notification channel %d: %w", index, err)
- }
- seen[channel] = struct{}{}
- }
- if err := tx.Commit(); err != nil {
- return fmt.Errorf("commit notification settings batch: %w", err)
- }
- return nil
- }
- func (s *Store) NotificationSetting(
- ctx context.Context,
- channel string,
- ) (NotificationSetting, error) {
- return notificationSetting(s.db.QueryRowContext(
- ctx,
- notificationSettingSelect+` WHERE channel = ?`,
- strings.ToLower(strings.TrimSpace(channel)),
- ))
- }
- func (s *Store) ListNotificationSettings(ctx context.Context) ([]NotificationSetting, error) {
- rows, err := s.db.QueryContext(ctx, notificationSettingSelect+` ORDER BY channel`)
- if err != nil {
- return nil, fmt.Errorf("list notification settings: %w", err)
- }
- defer rows.Close()
- values := make([]NotificationSetting, 0)
- for rows.Next() {
- value, err := notificationSetting(rows)
- if err != nil {
- return nil, fmt.Errorf("scan notification setting: %w", err)
- }
- values = append(values, value)
- }
- if err := rows.Err(); err != nil {
- return nil, fmt.Errorf("iterate notification settings: %w", err)
- }
- return values, nil
- }
- func (s *Store) DeleteNotificationSetting(ctx context.Context, channel string) error {
- result, err := s.db.ExecContext(
- ctx,
- `DELETE FROM notification_settings WHERE channel = ?`,
- strings.ToLower(strings.TrimSpace(channel)),
- )
- if err != nil {
- return fmt.Errorf("delete notification setting %q: %w", channel, err)
- }
- return requireAffected(result)
- }
- const notificationSettingSelect = `
- SELECT channel, enabled, config_json, sensitive_fields_json,
- created_at, updated_at
- FROM notification_settings`
- func notificationSetting(row rowScanner) (NotificationSetting, error) {
- var value NotificationSetting
- var enabled int
- var config, fields string
- var createdAt, updatedAt int64
- err := row.Scan(
- &value.Channel, &enabled, &config, &fields, &createdAt, &updatedAt,
- )
- if errors.Is(err, sql.ErrNoRows) {
- return NotificationSetting{}, ErrNotFound
- }
- if err != nil {
- return NotificationSetting{}, err
- }
- if err := json.Unmarshal([]byte(fields), &value.SensitiveFields); err != nil {
- return NotificationSetting{}, fmt.Errorf("decode sensitive fields: %w", err)
- }
- value.Enabled = enabled != 0
- value.Config = []byte(config)
- value.CreatedAt = time.Unix(createdAt, 0).UTC()
- value.UpdatedAt = time.Unix(updatedAt, 0).UTC()
- return value, nil
- }
- func uniqueNonemptyStrings(groups ...[]string) []string {
- seen := make(map[string]struct{})
- for _, group := range groups {
- for _, item := range group {
- item = strings.TrimSpace(item)
- if item != "" {
- seen[item] = struct{}{}
- }
- }
- }
- result := make([]string, 0, len(seen))
- for item := range seen {
- result = append(result, item)
- }
- sort.Strings(result)
- return result
- }
- func (s *Store) UpsertAppSetting(ctx context.Context, value AppSetting) error {
- tx, err := s.db.BeginTx(ctx, nil)
- if err != nil {
- return fmt.Errorf("begin app setting update: %w", err)
- }
- defer tx.Rollback()
- if err := upsertAppSetting(ctx, tx, value); err != nil {
- return err
- }
- if err := tx.Commit(); err != nil {
- return fmt.Errorf("commit app setting update: %w", err)
- }
- return nil
- }
- func upsertAppSetting(
- ctx context.Context,
- executor contextQueryExecer,
- value AppSetting,
- ) error {
- value.Key = strings.TrimSpace(value.Key)
- if value.Key == "" {
- return errors.New("app setting key is required")
- }
- normalized, err := normalizeJSONValue(value.Value)
- if err != nil {
- return fmt.Errorf("normalize app setting %q: %w", value.Key, err)
- }
- if value.Sensitive && maskedJSONValue(normalized) {
- current, currentErr := appSetting(executor.QueryRowContext(
- ctx,
- appSettingSelect+` WHERE key = ?`,
- value.Key,
- ))
- switch {
- case currentErr == nil:
- normalized = current.Value
- case errors.Is(currentErr, ErrNotFound):
- return fmt.Errorf("new sensitive app setting %q requires a value", value.Key)
- default:
- return fmt.Errorf("read app setting before update: %w", currentErr)
- }
- }
- updatedAt := value.UpdatedAt
- if updatedAt.IsZero() {
- updatedAt = time.Now().UTC()
- }
- _, err = executor.ExecContext(ctx, `
- INSERT INTO app_settings (key, value_json, sensitive, updated_at)
- VALUES (?, ?, ?, ?)
- ON CONFLICT(key) DO UPDATE SET
- value_json = excluded.value_json,
- sensitive = excluded.sensitive,
- updated_at = excluded.updated_at
- `, value.Key, string(normalized), boolInt(value.Sensitive), updatedAt.Unix())
- if err != nil {
- return fmt.Errorf("upsert app setting %q: %w", value.Key, err)
- }
- return nil
- }
- func (s *Store) AppSetting(ctx context.Context, key string) (AppSetting, error) {
- return appSetting(s.db.QueryRowContext(
- ctx,
- appSettingSelect+` WHERE key = ?`,
- strings.TrimSpace(key),
- ))
- }
- func (s *Store) ListAppSettings(ctx context.Context) ([]AppSetting, error) {
- rows, err := s.db.QueryContext(ctx, appSettingSelect+` ORDER BY key`)
- if err != nil {
- return nil, fmt.Errorf("list app settings: %w", err)
- }
- defer rows.Close()
- values := make([]AppSetting, 0)
- for rows.Next() {
- value, err := appSetting(rows)
- if err != nil {
- return nil, fmt.Errorf("scan app setting: %w", err)
- }
- values = append(values, value)
- }
- if err := rows.Err(); err != nil {
- return nil, fmt.Errorf("iterate app settings: %w", err)
- }
- return values, nil
- }
- func (s *Store) DeleteAppSetting(ctx context.Context, key string) error {
- result, err := s.db.ExecContext(ctx, `DELETE FROM app_settings WHERE key = ?`, key)
- if err != nil {
- return fmt.Errorf("delete app setting %q: %w", key, err)
- }
- return requireAffected(result)
- }
- const appSettingSelect = `
- SELECT key, value_json, sensitive, updated_at
- FROM app_settings`
- func appSetting(row rowScanner) (AppSetting, error) {
- var value AppSetting
- var sensitive int
- var raw string
- var updatedAt int64
- err := row.Scan(&value.Key, &raw, &sensitive, &updatedAt)
- if errors.Is(err, sql.ErrNoRows) {
- return AppSetting{}, ErrNotFound
- }
- if err != nil {
- return AppSetting{}, err
- }
- value.Value = []byte(raw)
- value.Sensitive = sensitive != 0
- value.UpdatedAt = time.Unix(updatedAt, 0).UTC()
- return value, nil
- }
- func maskedJSONValue(value json.RawMessage) bool {
- if bytes.Equal(bytes.TrimSpace(value), []byte(`null`)) {
- return true
- }
- var text string
- if json.Unmarshal(value, &text) == nil {
- return text == "" || text == SecretMask
- }
- return false
- }
- func (s *Store) UpsertCardPolicy(ctx context.Context, value CardPolicy) error {
- value.ICCID = strings.TrimSpace(value.ICCID)
- if value.ICCID == "" {
- return errors.New("card policy ICCID is required")
- }
- value.IPVersion = strings.ToUpper(strings.TrimSpace(value.IPVersion))
- switch value.IPVersion {
- case "", "IP", "IPV6", "IPV4V6":
- default:
- return fmt.Errorf("unsupported card policy IP version %q", value.IPVersion)
- }
- if value.VoWiFiEnabled && value.AirplaneEnabled {
- return errors.New("VoWiFi and airplane mode cannot both be enabled")
- }
- now := time.Now().UTC()
- createdAt := value.CreatedAt
- if createdAt.IsZero() {
- createdAt = now
- }
- updatedAt := value.UpdatedAt
- if updatedAt.IsZero() {
- updatedAt = now
- }
- _, err := s.db.ExecContext(ctx, `
- INSERT INTO card_policies (
- iccid, network_enabled, vowifi_enabled, airplane_enabled,
- apn, ip_version, source, created_at, updated_at
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
- ON CONFLICT(iccid) DO UPDATE SET
- network_enabled = excluded.network_enabled,
- vowifi_enabled = excluded.vowifi_enabled,
- airplane_enabled = excluded.airplane_enabled,
- apn = excluded.apn,
- ip_version = excluded.ip_version,
- source = excluded.source,
- updated_at = excluded.updated_at
- `,
- value.ICCID, boolInt(value.NetworkEnabled), boolInt(value.VoWiFiEnabled),
- boolInt(value.AirplaneEnabled), value.APN, value.IPVersion,
- value.Source, createdAt.Unix(), updatedAt.Unix(),
- )
- if err != nil {
- return fmt.Errorf("upsert card policy %q: %w", value.ICCID, err)
- }
- return nil
- }
- func (s *Store) CardPolicy(ctx context.Context, iccid string) (CardPolicy, error) {
- return cardPolicy(s.db.QueryRowContext(
- ctx,
- cardPolicySelect+` WHERE iccid = ?`,
- strings.TrimSpace(iccid),
- ))
- }
- func (s *Store) ListCardPolicies(ctx context.Context) ([]CardPolicy, error) {
- rows, err := s.db.QueryContext(ctx, cardPolicySelect+` ORDER BY iccid`)
- if err != nil {
- return nil, fmt.Errorf("list card policies: %w", err)
- }
- defer rows.Close()
- values := make([]CardPolicy, 0)
- for rows.Next() {
- value, err := cardPolicy(rows)
- if err != nil {
- return nil, fmt.Errorf("scan card policy: %w", err)
- }
- values = append(values, value)
- }
- if err := rows.Err(); err != nil {
- return nil, fmt.Errorf("iterate card policies: %w", err)
- }
- return values, nil
- }
- func (s *Store) DeleteCardPolicy(ctx context.Context, iccid string) error {
- result, err := s.db.ExecContext(ctx, `DELETE FROM card_policies WHERE iccid = ?`, iccid)
- if err != nil {
- return fmt.Errorf("delete card policy %q: %w", iccid, err)
- }
- return requireAffected(result)
- }
- const cardPolicySelect = `
- SELECT iccid, network_enabled, vowifi_enabled, airplane_enabled,
- apn, ip_version, source, created_at, updated_at
- FROM card_policies`
- func cardPolicy(row rowScanner) (CardPolicy, error) {
- var value CardPolicy
- var networkEnabled, vowifiEnabled, airplaneEnabled int
- var createdAt, updatedAt int64
- err := row.Scan(
- &value.ICCID, &networkEnabled, &vowifiEnabled, &airplaneEnabled,
- &value.APN, &value.IPVersion, &value.Source, &createdAt, &updatedAt,
- )
- if errors.Is(err, sql.ErrNoRows) {
- return CardPolicy{}, ErrNotFound
- }
- if err != nil {
- return CardPolicy{}, err
- }
- value.NetworkEnabled = networkEnabled != 0
- value.VoWiFiEnabled = vowifiEnabled != 0
- value.AirplaneEnabled = airplaneEnabled != 0
- value.CreatedAt = time.Unix(createdAt, 0).UTC()
- value.UpdatedAt = time.Unix(updatedAt, 0).UTC()
- return value, nil
- }
- func (s *Store) UpsertTrafficBucket(ctx context.Context, value TrafficBucket) error {
- return s.writeTrafficBucket(ctx, value, false)
- }
- // AddTrafficBucket atomically accumulates counters for concurrent collectors.
- func (s *Store) AddTrafficBucket(ctx context.Context, value TrafficBucket) error {
- return s.writeTrafficBucket(ctx, value, true)
- }
- func (s *Store) writeTrafficBucket(
- ctx context.Context,
- value TrafficBucket,
- accumulate bool,
- ) error {
- value.DeviceID = strings.TrimSpace(value.DeviceID)
- value.Bucket = strings.TrimSpace(value.Bucket)
- if value.DeviceID == "" || value.Bucket == "" {
- return errors.New("traffic bucket device id and bucket are required")
- }
- if value.PeriodStart.IsZero() {
- return errors.New("traffic bucket period start is required")
- }
- if value.RXBytes < 0 || value.TXBytes < 0 {
- return errors.New("traffic byte counters cannot be negative")
- }
- update := `
- rx_bytes = excluded.rx_bytes,
- tx_bytes = excluded.tx_bytes`
- if accumulate {
- update = `
- rx_bytes = traffic_buckets.rx_bytes + excluded.rx_bytes,
- tx_bytes = traffic_buckets.tx_bytes + excluded.tx_bytes`
- }
- _, err := s.db.ExecContext(ctx, `
- INSERT INTO traffic_buckets (
- device_id, bucket, period_start, rx_bytes, tx_bytes
- ) VALUES (?, ?, ?, ?, ?)
- ON CONFLICT(device_id, bucket, period_start) DO UPDATE SET`+update,
- value.DeviceID, value.Bucket, value.PeriodStart.UTC().Unix(),
- value.RXBytes, value.TXBytes,
- )
- if err != nil {
- return fmt.Errorf("write traffic bucket: %w", err)
- }
- return nil
- }
- func (s *Store) ListTrafficBuckets(
- ctx context.Context,
- filter TrafficFilter,
- ) ([]TrafficBucket, error) {
- clauses := make([]string, 0, 4)
- args := make([]any, 0, 5)
- if filter.DeviceID != "" {
- clauses = append(clauses, `device_id = ?`)
- args = append(args, filter.DeviceID)
- }
- if filter.Bucket != "" {
- clauses = append(clauses, `bucket = ?`)
- args = append(args, filter.Bucket)
- }
- if !filter.Since.IsZero() {
- clauses = append(clauses, `period_start >= ?`)
- args = append(args, filter.Since.UTC().Unix())
- }
- if !filter.Until.IsZero() {
- clauses = append(clauses, `period_start < ?`)
- args = append(args, filter.Until.UTC().Unix())
- }
- query := `
- SELECT device_id, bucket, period_start, rx_bytes, tx_bytes
- FROM traffic_buckets`
- if len(clauses) > 0 {
- query += ` WHERE ` + strings.Join(clauses, ` AND `)
- }
- query += ` ORDER BY period_start ASC, device_id LIMIT ?`
- args = append(args, normalizedLimit(filter.Limit))
- rows, err := s.db.QueryContext(ctx, query, args...)
- if err != nil {
- return nil, fmt.Errorf("list traffic buckets: %w", err)
- }
- defer rows.Close()
- values := make([]TrafficBucket, 0)
- for rows.Next() {
- var value TrafficBucket
- var periodStart int64
- if err := rows.Scan(
- &value.DeviceID, &value.Bucket, &periodStart,
- &value.RXBytes, &value.TXBytes,
- ); err != nil {
- return nil, fmt.Errorf("scan traffic bucket: %w", err)
- }
- value.PeriodStart = time.Unix(periodStart, 0).UTC()
- values = append(values, value)
- }
- if err := rows.Err(); err != nil {
- return nil, fmt.Errorf("iterate traffic buckets: %w", err)
- }
- return values, nil
- }
- func (s *Store) DeleteTrafficBefore(ctx context.Context, before time.Time) (int64, error) {
- result, err := s.db.ExecContext(
- ctx,
- `DELETE FROM traffic_buckets WHERE period_start < ?`,
- before.UTC().Unix(),
- )
- if err != nil {
- return 0, fmt.Errorf("delete old traffic buckets: %w", err)
- }
- affected, err := result.RowsAffected()
- if err != nil {
- return 0, fmt.Errorf("read deleted traffic bucket count: %w", err)
- }
- return affected, nil
- }
|