package remnawave // Local account storage: after the first successful provisioning the client // "remembers" the panel user in configs/account.json (never committed) and // from then on only renews that same user — it never creates a second one. import ( "bytes" "encoding/json" "fmt" "os" "path/filepath" "time" ) // Account is the locally remembered panel user. type Account struct { Username string `json:"username"` UUID string `json:"uuid"` ShortUUID string `json:"short_uuid,omitempty"` SubscriptionURL string `json:"subscription_url,omitempty"` TrafficLimitBytes int64 `json:"traffic_limit_bytes,omitempty"` ExpireAt int64 `json:"expire_at,omitempty"` // unix seconds; 0 = unknown CreatedAt int64 `json:"created_at,omitempty"` // unix seconds LastRenewedAt int64 `json:"last_renewed_at,omitempty"` // unix seconds; anti-loop guard LastRenewCheckAt int64 `json:"last_renew_check,omitempty"` // unix seconds of last auto check } // AccountPath returns configs/account.json next to the main config.json. func AccountPath(cfgPath string) string { return filepath.Join(filepath.Dir(cfgPath), "account.json") } // LoadAccount reads the remembered account; returns (nil, nil) when absent. func LoadAccount(path string) (*Account, error) { data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } var a Account if err := json.Unmarshal(bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF}), &a); err != nil { return nil, fmt.Errorf("account.json: некорректный JSON: %w", err) } if a.Username == "" && a.UUID == "" { return nil, nil } return &a, nil } // SaveAccount persists the account with restrictive permissions. func SaveAccount(path string, a *Account) error { if a == nil { return fmt.Errorf("пустой аккаунт") } data, err := json.MarshalIndent(a, "", " ") if err != nil { return err } data = append(data, '\n') if dir := filepath.Dir(path); dir != "." && dir != "" { if err := os.MkdirAll(dir, 0o755); err != nil { return err } } return os.WriteFile(path, data, 0o600) } // NeedsRenewal reports whether the plan should be renewed now: // expiry within 72h / past, or traffic exhausted (usedBytes vs limit). func (a *Account) NeedsRenewal(usedBytes int64, now time.Time) bool { if a == nil { return false } if a.ExpireAt > 0 && time.Unix(a.ExpireAt, 0).Before(now.Add(72*time.Hour)) { return true } if a.TrafficLimitBytes > 0 && usedBytes >= a.TrafficLimitBytes { return true } return false }