Release 3.9.0+3: subscription-URL routing, emoji flags, Remnawave provisioning.
Route credential-less http(s) URLs pasted into the add-link field to subscription import (fixes remaining 'proxy URI missing username'). Extend geoflag with RU country names and city hints; live Remnawave names already carrying emoji flags are kept as-is. Add admin provisioning via configs/remnawave-api.json (GET by-username / POST users, 50 GB MONTH plan) and the «Выдать доступ» UI panel. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -221,6 +221,13 @@ func (m *Manager) SetSystemProxy(enabled bool) {
|
||||
}
|
||||
|
||||
func (m *Manager) UpdateProxyURI(proxy string) error {
|
||||
// A plain http(s) URL without credentials is a subscription URL, not a
|
||||
// proxy link — run the subscription import instead of failing with
|
||||
// "proxy URI missing username".
|
||||
if linknorm.LooksLikeSubscriptionURL(proxy) {
|
||||
_, err := m.ImportSubscription(proxy)
|
||||
return err
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
active, err := m.cfg.ActiveProfile()
|
||||
@@ -253,6 +260,11 @@ func (m *Manager) SetActiveProfile(name string) error {
|
||||
}
|
||||
|
||||
func (m *Manager) SaveProfile(name, proxy string) error {
|
||||
// Subscription URL pasted into the "add server link" field → import it.
|
||||
if linknorm.LooksLikeSubscriptionURL(proxy) {
|
||||
_, err := m.ImportSubscription(proxy)
|
||||
return err
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.engine != nil && m.engine.Running() {
|
||||
@@ -279,6 +291,11 @@ func (m *Manager) SaveProfile(name, proxy string) error {
|
||||
|
||||
// SaveActiveProfile updates the current profile (rename if needed) and proxy URI.
|
||||
func (m *Manager) SaveActiveProfile(name, proxy string) error {
|
||||
// Subscription URL pasted into the profile link field → import it.
|
||||
if linknorm.LooksLikeSubscriptionURL(proxy) {
|
||||
_, err := m.ImportSubscription(proxy)
|
||||
return err
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.engine != nil && m.engine.Running() {
|
||||
@@ -576,6 +593,61 @@ func (m *Manager) applySubscription(ctx context.Context, res *subscription.Resul
|
||||
return ImportResult{Imported: len(res.Items), Skipped: res.Skipped, Warnings: res.Warnings}, nil
|
||||
}
|
||||
|
||||
// ProvisionOutcome is what the UI shows after issuing access via the panel.
|
||||
type ProvisionOutcome struct {
|
||||
Username string `json:"username"`
|
||||
Created bool `json:"created"`
|
||||
SubscriptionURL string `json:"subscription_url"`
|
||||
TrafficLimitBytes int64 `json:"traffic_limit_bytes,omitempty"`
|
||||
ExpireAt int64 `json:"expire_at,omitempty"`
|
||||
Imported int `json:"imported"`
|
||||
Skipped int `json:"skipped"`
|
||||
}
|
||||
|
||||
// ProvisionConfigured reports whether configs/remnawave-api.json exists —
|
||||
// the admin "Выдать доступ" panel is shown only in that case.
|
||||
func (m *Manager) ProvisionConfigured() bool {
|
||||
st, err := os.Stat(remnawave.AdminConfigPath(m.cfgPath))
|
||||
return err == nil && !st.IsDir()
|
||||
}
|
||||
|
||||
// ProvisionAccess creates (or finds) a panel user via the Remnawave admin API
|
||||
// and imports its subscription configs into the app.
|
||||
func (m *Manager) ProvisionAccess(username string) (ProvisionOutcome, error) {
|
||||
cfg, err := remnawave.LoadAdminConfig(remnawave.AdminConfigPath(m.cfgPath))
|
||||
if err != nil {
|
||||
return ProvisionOutcome{}, err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
res, err := remnawave.ProvisionUser(ctx, cfg, username)
|
||||
if err != nil {
|
||||
return ProvisionOutcome{}, err
|
||||
}
|
||||
out := ProvisionOutcome{
|
||||
Username: res.Username,
|
||||
Created: res.Created,
|
||||
SubscriptionURL: res.SubscriptionURL,
|
||||
TrafficLimitBytes: res.TrafficLimitBytes,
|
||||
ExpireAt: res.ExpireAt,
|
||||
}
|
||||
subURL := res.SubscriptionURL
|
||||
if subURL == "" && res.ShortUUID != "" {
|
||||
subURL = remnawave.PublicSubURL(cfg.PanelURL, res.ShortUUID)
|
||||
out.SubscriptionURL = subURL
|
||||
}
|
||||
if subURL == "" {
|
||||
return out, fmt.Errorf("панель не вернула subscription URL для %s", res.Username)
|
||||
}
|
||||
imp, err := m.ImportSubscription(subURL)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("доступ выдан (%s), но импорт конфигов не удался: %w", res.Username, err)
|
||||
}
|
||||
out.Imported = imp.Imported
|
||||
out.Skipped = imp.Skipped
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func buildSubInfo(info *subscription.Info) *config.SubscriptionInfo {
|
||||
if info == nil {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user