settings.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. package store
  2. import (
  3. "bytes"
  4. "context"
  5. "database/sql"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "sort"
  10. "strings"
  11. "time"
  12. )
  13. func DefaultNotificationSensitiveFields(channel string) []string {
  14. switch strings.ToLower(strings.TrimSpace(channel)) {
  15. case "telegram":
  16. return []string{"bot_token"}
  17. case "email":
  18. return []string{"password"}
  19. case "webhook":
  20. return []string{"secret"}
  21. case "pushplus":
  22. return []string{"token"}
  23. default:
  24. return nil
  25. }
  26. }
  27. func (s *Store) UpsertNotificationSetting(
  28. ctx context.Context,
  29. value NotificationSetting,
  30. ) error {
  31. tx, err := s.db.BeginTx(ctx, nil)
  32. if err != nil {
  33. return fmt.Errorf("begin notification setting update: %w", err)
  34. }
  35. defer tx.Rollback()
  36. if err := upsertNotificationSetting(ctx, tx, value); err != nil {
  37. return err
  38. }
  39. if err := tx.Commit(); err != nil {
  40. return fmt.Errorf("commit notification setting update: %w", err)
  41. }
  42. return nil
  43. }
  44. func upsertNotificationSetting(
  45. ctx context.Context,
  46. executor contextQueryExecer,
  47. value NotificationSetting,
  48. ) error {
  49. value.Channel = strings.ToLower(strings.TrimSpace(value.Channel))
  50. if value.Channel == "" {
  51. return errors.New("notification channel is required")
  52. }
  53. config, err := normalizeJSONObject(value.Config)
  54. if err != nil {
  55. return fmt.Errorf("normalize %s notification config: %w", value.Channel, err)
  56. }
  57. current, currentErr := notificationSetting(executor.QueryRowContext(
  58. ctx,
  59. notificationSettingSelect+` WHERE channel = ?`,
  60. value.Channel,
  61. ))
  62. if currentErr != nil && !errors.Is(currentErr, ErrNotFound) {
  63. return fmt.Errorf("read %s notification setting before update: %w", value.Channel, currentErr)
  64. }
  65. fields := uniqueNonemptyStrings(
  66. DefaultNotificationSensitiveFields(value.Channel),
  67. value.SensitiveFields,
  68. )
  69. if currentErr == nil {
  70. fields = uniqueNonemptyStrings(fields, current.SensitiveFields)
  71. config, err = mergeJSONSecrets(config, current.Config, fields)
  72. if err != nil {
  73. return fmt.Errorf("preserve %s notification secrets: %w", value.Channel, err)
  74. }
  75. }
  76. fieldsJSON, err := json.Marshal(fields)
  77. if err != nil {
  78. return fmt.Errorf("encode notification sensitive fields: %w", err)
  79. }
  80. now := time.Now().UTC()
  81. createdAt := value.CreatedAt
  82. if createdAt.IsZero() {
  83. createdAt = now
  84. }
  85. updatedAt := value.UpdatedAt
  86. if updatedAt.IsZero() {
  87. updatedAt = now
  88. }
  89. _, err = executor.ExecContext(ctx, `
  90. INSERT INTO notification_settings (
  91. channel, enabled, config_json, sensitive_fields_json,
  92. created_at, updated_at
  93. ) VALUES (?, ?, ?, ?, ?, ?)
  94. ON CONFLICT(channel) DO UPDATE SET
  95. enabled = excluded.enabled,
  96. config_json = excluded.config_json,
  97. sensitive_fields_json = excluded.sensitive_fields_json,
  98. updated_at = excluded.updated_at
  99. `,
  100. value.Channel, boolInt(value.Enabled), string(config),
  101. string(fieldsJSON), createdAt.Unix(), updatedAt.Unix(),
  102. )
  103. if err != nil {
  104. return fmt.Errorf("upsert %s notification setting: %w", value.Channel, err)
  105. }
  106. return nil
  107. }
  108. // SaveNotificationSettings applies a multi-channel settings form atomically.
  109. func (s *Store) SaveNotificationSettings(
  110. ctx context.Context,
  111. values []NotificationSetting,
  112. ) error {
  113. tx, err := s.db.BeginTx(ctx, nil)
  114. if err != nil {
  115. return fmt.Errorf("begin notification settings batch: %w", err)
  116. }
  117. defer tx.Rollback()
  118. seen := make(map[string]struct{}, len(values))
  119. for index, value := range values {
  120. channel := strings.ToLower(strings.TrimSpace(value.Channel))
  121. if _, duplicate := seen[channel]; duplicate {
  122. return fmt.Errorf("duplicate notification channel %q", channel)
  123. }
  124. if err := upsertNotificationSetting(ctx, tx, value); err != nil {
  125. return fmt.Errorf("save notification channel %d: %w", index, err)
  126. }
  127. seen[channel] = struct{}{}
  128. }
  129. if err := tx.Commit(); err != nil {
  130. return fmt.Errorf("commit notification settings batch: %w", err)
  131. }
  132. return nil
  133. }
  134. func (s *Store) NotificationSetting(
  135. ctx context.Context,
  136. channel string,
  137. ) (NotificationSetting, error) {
  138. return notificationSetting(s.db.QueryRowContext(
  139. ctx,
  140. notificationSettingSelect+` WHERE channel = ?`,
  141. strings.ToLower(strings.TrimSpace(channel)),
  142. ))
  143. }
  144. func (s *Store) ListNotificationSettings(ctx context.Context) ([]NotificationSetting, error) {
  145. rows, err := s.db.QueryContext(ctx, notificationSettingSelect+` ORDER BY channel`)
  146. if err != nil {
  147. return nil, fmt.Errorf("list notification settings: %w", err)
  148. }
  149. defer rows.Close()
  150. values := make([]NotificationSetting, 0)
  151. for rows.Next() {
  152. value, err := notificationSetting(rows)
  153. if err != nil {
  154. return nil, fmt.Errorf("scan notification setting: %w", err)
  155. }
  156. values = append(values, value)
  157. }
  158. if err := rows.Err(); err != nil {
  159. return nil, fmt.Errorf("iterate notification settings: %w", err)
  160. }
  161. return values, nil
  162. }
  163. func (s *Store) DeleteNotificationSetting(ctx context.Context, channel string) error {
  164. result, err := s.db.ExecContext(
  165. ctx,
  166. `DELETE FROM notification_settings WHERE channel = ?`,
  167. strings.ToLower(strings.TrimSpace(channel)),
  168. )
  169. if err != nil {
  170. return fmt.Errorf("delete notification setting %q: %w", channel, err)
  171. }
  172. return requireAffected(result)
  173. }
  174. const notificationSettingSelect = `
  175. SELECT channel, enabled, config_json, sensitive_fields_json,
  176. created_at, updated_at
  177. FROM notification_settings`
  178. func notificationSetting(row rowScanner) (NotificationSetting, error) {
  179. var value NotificationSetting
  180. var enabled int
  181. var config, fields string
  182. var createdAt, updatedAt int64
  183. err := row.Scan(
  184. &value.Channel, &enabled, &config, &fields, &createdAt, &updatedAt,
  185. )
  186. if errors.Is(err, sql.ErrNoRows) {
  187. return NotificationSetting{}, ErrNotFound
  188. }
  189. if err != nil {
  190. return NotificationSetting{}, err
  191. }
  192. if err := json.Unmarshal([]byte(fields), &value.SensitiveFields); err != nil {
  193. return NotificationSetting{}, fmt.Errorf("decode sensitive fields: %w", err)
  194. }
  195. value.Enabled = enabled != 0
  196. value.Config = []byte(config)
  197. value.CreatedAt = time.Unix(createdAt, 0).UTC()
  198. value.UpdatedAt = time.Unix(updatedAt, 0).UTC()
  199. return value, nil
  200. }
  201. func uniqueNonemptyStrings(groups ...[]string) []string {
  202. seen := make(map[string]struct{})
  203. for _, group := range groups {
  204. for _, item := range group {
  205. item = strings.TrimSpace(item)
  206. if item != "" {
  207. seen[item] = struct{}{}
  208. }
  209. }
  210. }
  211. result := make([]string, 0, len(seen))
  212. for item := range seen {
  213. result = append(result, item)
  214. }
  215. sort.Strings(result)
  216. return result
  217. }
  218. func (s *Store) UpsertAppSetting(ctx context.Context, value AppSetting) error {
  219. tx, err := s.db.BeginTx(ctx, nil)
  220. if err != nil {
  221. return fmt.Errorf("begin app setting update: %w", err)
  222. }
  223. defer tx.Rollback()
  224. if err := upsertAppSetting(ctx, tx, value); err != nil {
  225. return err
  226. }
  227. if err := tx.Commit(); err != nil {
  228. return fmt.Errorf("commit app setting update: %w", err)
  229. }
  230. return nil
  231. }
  232. func upsertAppSetting(
  233. ctx context.Context,
  234. executor contextQueryExecer,
  235. value AppSetting,
  236. ) error {
  237. value.Key = strings.TrimSpace(value.Key)
  238. if value.Key == "" {
  239. return errors.New("app setting key is required")
  240. }
  241. normalized, err := normalizeJSONValue(value.Value)
  242. if err != nil {
  243. return fmt.Errorf("normalize app setting %q: %w", value.Key, err)
  244. }
  245. if value.Sensitive && maskedJSONValue(normalized) {
  246. current, currentErr := appSetting(executor.QueryRowContext(
  247. ctx,
  248. appSettingSelect+` WHERE key = ?`,
  249. value.Key,
  250. ))
  251. switch {
  252. case currentErr == nil:
  253. normalized = current.Value
  254. case errors.Is(currentErr, ErrNotFound):
  255. return fmt.Errorf("new sensitive app setting %q requires a value", value.Key)
  256. default:
  257. return fmt.Errorf("read app setting before update: %w", currentErr)
  258. }
  259. }
  260. updatedAt := value.UpdatedAt
  261. if updatedAt.IsZero() {
  262. updatedAt = time.Now().UTC()
  263. }
  264. _, err = executor.ExecContext(ctx, `
  265. INSERT INTO app_settings (key, value_json, sensitive, updated_at)
  266. VALUES (?, ?, ?, ?)
  267. ON CONFLICT(key) DO UPDATE SET
  268. value_json = excluded.value_json,
  269. sensitive = excluded.sensitive,
  270. updated_at = excluded.updated_at
  271. `, value.Key, string(normalized), boolInt(value.Sensitive), updatedAt.Unix())
  272. if err != nil {
  273. return fmt.Errorf("upsert app setting %q: %w", value.Key, err)
  274. }
  275. return nil
  276. }
  277. func (s *Store) AppSetting(ctx context.Context, key string) (AppSetting, error) {
  278. return appSetting(s.db.QueryRowContext(
  279. ctx,
  280. appSettingSelect+` WHERE key = ?`,
  281. strings.TrimSpace(key),
  282. ))
  283. }
  284. func (s *Store) ListAppSettings(ctx context.Context) ([]AppSetting, error) {
  285. rows, err := s.db.QueryContext(ctx, appSettingSelect+` ORDER BY key`)
  286. if err != nil {
  287. return nil, fmt.Errorf("list app settings: %w", err)
  288. }
  289. defer rows.Close()
  290. values := make([]AppSetting, 0)
  291. for rows.Next() {
  292. value, err := appSetting(rows)
  293. if err != nil {
  294. return nil, fmt.Errorf("scan app setting: %w", err)
  295. }
  296. values = append(values, value)
  297. }
  298. if err := rows.Err(); err != nil {
  299. return nil, fmt.Errorf("iterate app settings: %w", err)
  300. }
  301. return values, nil
  302. }
  303. func (s *Store) DeleteAppSetting(ctx context.Context, key string) error {
  304. result, err := s.db.ExecContext(ctx, `DELETE FROM app_settings WHERE key = ?`, key)
  305. if err != nil {
  306. return fmt.Errorf("delete app setting %q: %w", key, err)
  307. }
  308. return requireAffected(result)
  309. }
  310. const appSettingSelect = `
  311. SELECT key, value_json, sensitive, updated_at
  312. FROM app_settings`
  313. func appSetting(row rowScanner) (AppSetting, error) {
  314. var value AppSetting
  315. var sensitive int
  316. var raw string
  317. var updatedAt int64
  318. err := row.Scan(&value.Key, &raw, &sensitive, &updatedAt)
  319. if errors.Is(err, sql.ErrNoRows) {
  320. return AppSetting{}, ErrNotFound
  321. }
  322. if err != nil {
  323. return AppSetting{}, err
  324. }
  325. value.Value = []byte(raw)
  326. value.Sensitive = sensitive != 0
  327. value.UpdatedAt = time.Unix(updatedAt, 0).UTC()
  328. return value, nil
  329. }
  330. func maskedJSONValue(value json.RawMessage) bool {
  331. if bytes.Equal(bytes.TrimSpace(value), []byte(`null`)) {
  332. return true
  333. }
  334. var text string
  335. if json.Unmarshal(value, &text) == nil {
  336. return text == "" || text == SecretMask
  337. }
  338. return false
  339. }
  340. func (s *Store) UpsertCardPolicy(ctx context.Context, value CardPolicy) error {
  341. value.ICCID = strings.TrimSpace(value.ICCID)
  342. if value.ICCID == "" {
  343. return errors.New("card policy ICCID is required")
  344. }
  345. value.IPVersion = strings.ToUpper(strings.TrimSpace(value.IPVersion))
  346. switch value.IPVersion {
  347. case "", "IP", "IPV6", "IPV4V6":
  348. default:
  349. return fmt.Errorf("unsupported card policy IP version %q", value.IPVersion)
  350. }
  351. if value.VoWiFiEnabled && value.AirplaneEnabled {
  352. return errors.New("VoWiFi and airplane mode cannot both be enabled")
  353. }
  354. now := time.Now().UTC()
  355. createdAt := value.CreatedAt
  356. if createdAt.IsZero() {
  357. createdAt = now
  358. }
  359. updatedAt := value.UpdatedAt
  360. if updatedAt.IsZero() {
  361. updatedAt = now
  362. }
  363. _, err := s.db.ExecContext(ctx, `
  364. INSERT INTO card_policies (
  365. iccid, network_enabled, vowifi_enabled, airplane_enabled,
  366. apn, ip_version, source, created_at, updated_at
  367. ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
  368. ON CONFLICT(iccid) DO UPDATE SET
  369. network_enabled = excluded.network_enabled,
  370. vowifi_enabled = excluded.vowifi_enabled,
  371. airplane_enabled = excluded.airplane_enabled,
  372. apn = excluded.apn,
  373. ip_version = excluded.ip_version,
  374. source = excluded.source,
  375. updated_at = excluded.updated_at
  376. `,
  377. value.ICCID, boolInt(value.NetworkEnabled), boolInt(value.VoWiFiEnabled),
  378. boolInt(value.AirplaneEnabled), value.APN, value.IPVersion,
  379. value.Source, createdAt.Unix(), updatedAt.Unix(),
  380. )
  381. if err != nil {
  382. return fmt.Errorf("upsert card policy %q: %w", value.ICCID, err)
  383. }
  384. return nil
  385. }
  386. func (s *Store) CardPolicy(ctx context.Context, iccid string) (CardPolicy, error) {
  387. return cardPolicy(s.db.QueryRowContext(
  388. ctx,
  389. cardPolicySelect+` WHERE iccid = ?`,
  390. strings.TrimSpace(iccid),
  391. ))
  392. }
  393. func (s *Store) ListCardPolicies(ctx context.Context) ([]CardPolicy, error) {
  394. rows, err := s.db.QueryContext(ctx, cardPolicySelect+` ORDER BY iccid`)
  395. if err != nil {
  396. return nil, fmt.Errorf("list card policies: %w", err)
  397. }
  398. defer rows.Close()
  399. values := make([]CardPolicy, 0)
  400. for rows.Next() {
  401. value, err := cardPolicy(rows)
  402. if err != nil {
  403. return nil, fmt.Errorf("scan card policy: %w", err)
  404. }
  405. values = append(values, value)
  406. }
  407. if err := rows.Err(); err != nil {
  408. return nil, fmt.Errorf("iterate card policies: %w", err)
  409. }
  410. return values, nil
  411. }
  412. func (s *Store) DeleteCardPolicy(ctx context.Context, iccid string) error {
  413. result, err := s.db.ExecContext(ctx, `DELETE FROM card_policies WHERE iccid = ?`, iccid)
  414. if err != nil {
  415. return fmt.Errorf("delete card policy %q: %w", iccid, err)
  416. }
  417. return requireAffected(result)
  418. }
  419. const cardPolicySelect = `
  420. SELECT iccid, network_enabled, vowifi_enabled, airplane_enabled,
  421. apn, ip_version, source, created_at, updated_at
  422. FROM card_policies`
  423. func cardPolicy(row rowScanner) (CardPolicy, error) {
  424. var value CardPolicy
  425. var networkEnabled, vowifiEnabled, airplaneEnabled int
  426. var createdAt, updatedAt int64
  427. err := row.Scan(
  428. &value.ICCID, &networkEnabled, &vowifiEnabled, &airplaneEnabled,
  429. &value.APN, &value.IPVersion, &value.Source, &createdAt, &updatedAt,
  430. )
  431. if errors.Is(err, sql.ErrNoRows) {
  432. return CardPolicy{}, ErrNotFound
  433. }
  434. if err != nil {
  435. return CardPolicy{}, err
  436. }
  437. value.NetworkEnabled = networkEnabled != 0
  438. value.VoWiFiEnabled = vowifiEnabled != 0
  439. value.AirplaneEnabled = airplaneEnabled != 0
  440. value.CreatedAt = time.Unix(createdAt, 0).UTC()
  441. value.UpdatedAt = time.Unix(updatedAt, 0).UTC()
  442. return value, nil
  443. }
  444. func (s *Store) UpsertTrafficBucket(ctx context.Context, value TrafficBucket) error {
  445. return s.writeTrafficBucket(ctx, value, false)
  446. }
  447. // AddTrafficBucket atomically accumulates counters for concurrent collectors.
  448. func (s *Store) AddTrafficBucket(ctx context.Context, value TrafficBucket) error {
  449. return s.writeTrafficBucket(ctx, value, true)
  450. }
  451. func (s *Store) writeTrafficBucket(
  452. ctx context.Context,
  453. value TrafficBucket,
  454. accumulate bool,
  455. ) error {
  456. value.DeviceID = strings.TrimSpace(value.DeviceID)
  457. value.Bucket = strings.TrimSpace(value.Bucket)
  458. if value.DeviceID == "" || value.Bucket == "" {
  459. return errors.New("traffic bucket device id and bucket are required")
  460. }
  461. if value.PeriodStart.IsZero() {
  462. return errors.New("traffic bucket period start is required")
  463. }
  464. if value.RXBytes < 0 || value.TXBytes < 0 {
  465. return errors.New("traffic byte counters cannot be negative")
  466. }
  467. update := `
  468. rx_bytes = excluded.rx_bytes,
  469. tx_bytes = excluded.tx_bytes`
  470. if accumulate {
  471. update = `
  472. rx_bytes = traffic_buckets.rx_bytes + excluded.rx_bytes,
  473. tx_bytes = traffic_buckets.tx_bytes + excluded.tx_bytes`
  474. }
  475. _, err := s.db.ExecContext(ctx, `
  476. INSERT INTO traffic_buckets (
  477. device_id, bucket, period_start, rx_bytes, tx_bytes
  478. ) VALUES (?, ?, ?, ?, ?)
  479. ON CONFLICT(device_id, bucket, period_start) DO UPDATE SET`+update,
  480. value.DeviceID, value.Bucket, value.PeriodStart.UTC().Unix(),
  481. value.RXBytes, value.TXBytes,
  482. )
  483. if err != nil {
  484. return fmt.Errorf("write traffic bucket: %w", err)
  485. }
  486. return nil
  487. }
  488. func (s *Store) ListTrafficBuckets(
  489. ctx context.Context,
  490. filter TrafficFilter,
  491. ) ([]TrafficBucket, error) {
  492. clauses := make([]string, 0, 4)
  493. args := make([]any, 0, 5)
  494. if filter.DeviceID != "" {
  495. clauses = append(clauses, `device_id = ?`)
  496. args = append(args, filter.DeviceID)
  497. }
  498. if filter.Bucket != "" {
  499. clauses = append(clauses, `bucket = ?`)
  500. args = append(args, filter.Bucket)
  501. }
  502. if !filter.Since.IsZero() {
  503. clauses = append(clauses, `period_start >= ?`)
  504. args = append(args, filter.Since.UTC().Unix())
  505. }
  506. if !filter.Until.IsZero() {
  507. clauses = append(clauses, `period_start < ?`)
  508. args = append(args, filter.Until.UTC().Unix())
  509. }
  510. query := `
  511. SELECT device_id, bucket, period_start, rx_bytes, tx_bytes
  512. FROM traffic_buckets`
  513. if len(clauses) > 0 {
  514. query += ` WHERE ` + strings.Join(clauses, ` AND `)
  515. }
  516. query += ` ORDER BY period_start ASC, device_id LIMIT ?`
  517. args = append(args, normalizedLimit(filter.Limit))
  518. rows, err := s.db.QueryContext(ctx, query, args...)
  519. if err != nil {
  520. return nil, fmt.Errorf("list traffic buckets: %w", err)
  521. }
  522. defer rows.Close()
  523. values := make([]TrafficBucket, 0)
  524. for rows.Next() {
  525. var value TrafficBucket
  526. var periodStart int64
  527. if err := rows.Scan(
  528. &value.DeviceID, &value.Bucket, &periodStart,
  529. &value.RXBytes, &value.TXBytes,
  530. ); err != nil {
  531. return nil, fmt.Errorf("scan traffic bucket: %w", err)
  532. }
  533. value.PeriodStart = time.Unix(periodStart, 0).UTC()
  534. values = append(values, value)
  535. }
  536. if err := rows.Err(); err != nil {
  537. return nil, fmt.Errorf("iterate traffic buckets: %w", err)
  538. }
  539. return values, nil
  540. }
  541. func (s *Store) DeleteTrafficBefore(ctx context.Context, before time.Time) (int64, error) {
  542. result, err := s.db.ExecContext(
  543. ctx,
  544. `DELETE FROM traffic_buckets WHERE period_start < ?`,
  545. before.UTC().Unix(),
  546. )
  547. if err != nil {
  548. return 0, fmt.Errorf("delete old traffic buckets: %w", err)
  549. }
  550. affected, err := result.RowsAffected()
  551. if err != nil {
  552. return 0, fmt.Errorf("read deleted traffic bucket count: %w", err)
  553. }
  554. return affected, nil
  555. }