mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
743 lines
23 KiB
Go
743 lines
23 KiB
Go
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"}
|
|
case "wecom":
|
|
return []string{"urls"}
|
|
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)
|
|
value.CustomPhoneNumber = strings.TrimSpace(value.CustomPhoneNumber)
|
|
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)
|
|
}
|
|
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, custom_phone_number, 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,
|
|
custom_phone_number = excluded.custom_phone_number,
|
|
source = excluded.source,
|
|
updated_at = excluded.updated_at
|
|
`,
|
|
value.ICCID, boolInt(value.NetworkEnabled), boolInt(value.VoWiFiEnabled),
|
|
boolInt(value.AirplaneEnabled), value.APN, value.IPVersion,
|
|
value.CustomPhoneNumber, 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, custom_phone_number, 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.CustomPhoneNumber, &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) UpsertCardAPNProfile(ctx context.Context, value CardAPNProfile) (CardAPNProfile, error) {
|
|
value.ICCID = strings.TrimSpace(value.ICCID)
|
|
value.APN = strings.TrimSpace(value.APN)
|
|
value.IPVersion = strings.ToUpper(strings.TrimSpace(value.IPVersion))
|
|
if value.ICCID == "" || value.APN == "" {
|
|
return CardAPNProfile{}, errors.New("card APN profile ICCID and APN are required")
|
|
}
|
|
if value.IPVersion == "" {
|
|
value.IPVersion = "IPV4V6"
|
|
}
|
|
switch value.IPVersion {
|
|
case "IP", "IPV6", "IPV4V6":
|
|
default:
|
|
return CardAPNProfile{}, fmt.Errorf("unsupported card APN profile IP version %q", value.IPVersion)
|
|
}
|
|
value.RoamingIPVersion = strings.ToUpper(strings.TrimSpace(value.RoamingIPVersion))
|
|
if value.RoamingIPVersion == "" {
|
|
value.RoamingIPVersion = "IP"
|
|
}
|
|
switch value.RoamingIPVersion {
|
|
case "IP", "IPV6", "IPV4V6":
|
|
default:
|
|
return CardAPNProfile{}, fmt.Errorf("unsupported card APN roaming IP version %q", value.RoamingIPVersion)
|
|
}
|
|
value.AuthType = strings.ToUpper(strings.TrimSpace(value.AuthType))
|
|
if value.AuthType == "" {
|
|
value.AuthType = "NONE"
|
|
}
|
|
switch value.AuthType {
|
|
case "NONE", "PAP", "CHAP", "PAP_OR_CHAP":
|
|
default:
|
|
return CardAPNProfile{}, fmt.Errorf("unsupported card APN authentication type %q", value.AuthType)
|
|
}
|
|
value.Username = strings.TrimSpace(value.Username)
|
|
value.Proxy = strings.TrimSpace(value.Proxy)
|
|
value.MCC = strings.TrimSpace(value.MCC)
|
|
value.MNC = strings.TrimSpace(value.MNC)
|
|
now := time.Now().UTC().Unix()
|
|
var createdAt, updatedAt int64
|
|
err := s.db.QueryRowContext(ctx, `
|
|
INSERT INTO card_apn_profiles (
|
|
iccid, apn, username, password, proxy, mcc, mnc,
|
|
ip_version, roaming_ip_version, auth_type, created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(iccid, apn, ip_version) DO UPDATE SET
|
|
username = excluded.username, password = excluded.password,
|
|
proxy = excluded.proxy, mcc = excluded.mcc, mnc = excluded.mnc,
|
|
roaming_ip_version = excluded.roaming_ip_version,
|
|
auth_type = excluded.auth_type, updated_at = excluded.updated_at
|
|
RETURNING id, iccid, apn, username, password, proxy, mcc, mnc,
|
|
ip_version, roaming_ip_version, auth_type, created_at, updated_at
|
|
`, value.ICCID, value.APN, value.Username, value.Password, value.Proxy, value.MCC, value.MNC,
|
|
value.IPVersion, value.RoamingIPVersion, value.AuthType, now, now).Scan(
|
|
&value.ID, &value.ICCID, &value.APN, &value.Username, &value.Password,
|
|
&value.Proxy, &value.MCC, &value.MNC, &value.IPVersion,
|
|
&value.RoamingIPVersion, &value.AuthType, &createdAt, &updatedAt,
|
|
)
|
|
if err != nil {
|
|
return CardAPNProfile{}, fmt.Errorf("upsert card APN profile: %w", err)
|
|
}
|
|
value.CreatedAt = time.Unix(createdAt, 0).UTC()
|
|
value.UpdatedAt = time.Unix(updatedAt, 0).UTC()
|
|
return value, nil
|
|
}
|
|
|
|
func (s *Store) ListCardAPNProfiles(ctx context.Context, iccid string) ([]CardAPNProfile, error) {
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT id, iccid, apn, username, password, proxy, mcc, mnc,
|
|
ip_version, roaming_ip_version, auth_type, created_at, updated_at
|
|
FROM card_apn_profiles WHERE iccid = ? ORDER BY id
|
|
`, strings.TrimSpace(iccid))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list card APN profiles: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
values := make([]CardAPNProfile, 0)
|
|
for rows.Next() {
|
|
var value CardAPNProfile
|
|
var createdAt, updatedAt int64
|
|
if err := rows.Scan(&value.ID, &value.ICCID, &value.APN, &value.Username,
|
|
&value.Password, &value.Proxy, &value.MCC, &value.MNC, &value.IPVersion,
|
|
&value.RoamingIPVersion, &value.AuthType, &createdAt, &updatedAt); err != nil {
|
|
return nil, fmt.Errorf("scan card APN profile: %w", err)
|
|
}
|
|
value.CreatedAt = time.Unix(createdAt, 0).UTC()
|
|
value.UpdatedAt = time.Unix(updatedAt, 0).UTC()
|
|
values = append(values, value)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate card APN profiles: %w", err)
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
func (s *Store) CardAPNProfileByAPN(ctx context.Context, iccid, apn, ipVersion string) (CardAPNProfile, error) {
|
|
profiles, err := s.ListCardAPNProfiles(ctx, iccid)
|
|
if err != nil {
|
|
return CardAPNProfile{}, err
|
|
}
|
|
for _, profile := range profiles {
|
|
if strings.EqualFold(profile.APN, strings.TrimSpace(apn)) &&
|
|
strings.EqualFold(profile.IPVersion, strings.TrimSpace(ipVersion)) {
|
|
return profile, nil
|
|
}
|
|
}
|
|
return CardAPNProfile{}, ErrNotFound
|
|
}
|
|
|
|
func (s *Store) UpdateCardAPNProfile(ctx context.Context, value CardAPNProfile) (CardAPNProfile, error) {
|
|
value.ICCID = strings.TrimSpace(value.ICCID)
|
|
value.APN = strings.TrimSpace(value.APN)
|
|
value.Username = strings.TrimSpace(value.Username)
|
|
value.Proxy = strings.TrimSpace(value.Proxy)
|
|
value.MCC = strings.TrimSpace(value.MCC)
|
|
value.MNC = strings.TrimSpace(value.MNC)
|
|
value.IPVersion = strings.ToUpper(strings.TrimSpace(value.IPVersion))
|
|
value.RoamingIPVersion = strings.ToUpper(strings.TrimSpace(value.RoamingIPVersion))
|
|
value.AuthType = strings.ToUpper(strings.TrimSpace(value.AuthType))
|
|
if value.ID < 1 || value.ICCID == "" || value.APN == "" {
|
|
return CardAPNProfile{}, errors.New("card APN profile ID, ICCID, and APN are required")
|
|
}
|
|
now := time.Now().UTC().Unix()
|
|
var createdAt, updatedAt int64
|
|
err := s.db.QueryRowContext(ctx, `
|
|
UPDATE card_apn_profiles SET
|
|
apn = ?, username = ?, password = ?, proxy = ?, mcc = ?, mnc = ?,
|
|
ip_version = ?, roaming_ip_version = ?, auth_type = ?, updated_at = ?
|
|
WHERE id = ? AND iccid = ?
|
|
RETURNING id, iccid, apn, username, password, proxy, mcc, mnc,
|
|
ip_version, roaming_ip_version, auth_type, created_at, updated_at
|
|
`, value.APN, value.Username, value.Password, value.Proxy, value.MCC, value.MNC,
|
|
value.IPVersion, value.RoamingIPVersion, value.AuthType, now, value.ID, value.ICCID).Scan(
|
|
&value.ID, &value.ICCID, &value.APN, &value.Username, &value.Password,
|
|
&value.Proxy, &value.MCC, &value.MNC, &value.IPVersion,
|
|
&value.RoamingIPVersion, &value.AuthType, &createdAt, &updatedAt,
|
|
)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return CardAPNProfile{}, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return CardAPNProfile{}, fmt.Errorf("update card APN profile: %w", err)
|
|
}
|
|
value.CreatedAt = time.Unix(createdAt, 0).UTC()
|
|
value.UpdatedAt = time.Unix(updatedAt, 0).UTC()
|
|
return value, nil
|
|
}
|
|
|
|
func (s *Store) DeleteCardAPNProfile(ctx context.Context, iccid string, id int64) error {
|
|
result, err := s.db.ExecContext(ctx, `DELETE FROM card_apn_profiles WHERE iccid = ? AND id = ?`, strings.TrimSpace(iccid), id)
|
|
if err != nil {
|
|
return fmt.Errorf("delete card APN profile: %w", err)
|
|
}
|
|
return requireAffected(result)
|
|
}
|
|
|
|
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
|
|
}
|