Release 3.10.0+1: sidebar UI (Home / Profiles / Settings / Logs / About), remembered account with 50 GB monthly renew + auto-renew, Windows VPN mode (wintun + tun2socks).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user