package sub2api import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "net/http" "net/url" "strconv" "strings" ) const ( managedSubscriptionBalance = 10.0 managedSubscriptionValidityDays = 30 ) type adminUserRecord struct { ID int64 `json:"id"` Email string `json:"email"` } type adminAPIKeyRecord struct { ID int64 `json:"id"` Key string `json:"key"` Name string `json:"name"` Group *struct { ID int64 `json:"id"` } `json:"group,omitempty"` GroupID *int64 `json:"group_id,omitempty"` } type authTokenPair struct { AccessToken string `json:"access_token"` } func (c *Client) EnsureSubscriptionAccess(ctx context.Context, req EnsureSubscriptionAccessRequest) (SubscriptionAccessRef, error) { if c == nil { return SubscriptionAccessRef{}, fmt.Errorf("client is required") } selector := strings.TrimSpace(req.UserSelector) groupID := strings.TrimSpace(req.GroupID) if selector == "" { return SubscriptionAccessRef{}, fmt.Errorf("user selector is required") } if groupID == "" { return SubscriptionAccessRef{}, fmt.Errorf("group id is required") } groupInt, err := strconv.ParseInt(groupID, 10, 64) if err != nil { return SubscriptionAccessRef{}, fmt.Errorf("parse group id %q: %w", groupID, err) } if probeAPIKey := strings.TrimSpace(req.ProbeAPIKey); probeAPIKey != "" { return c.ensureRealSubscriptionAccess(ctx, selector, groupInt, probeAPIKey) } identity := buildManagedSubscriptionIdentity(selector, groupID) user, err := c.findManagedSubscriptionUser(ctx, identity.Email) if err != nil { return SubscriptionAccessRef{}, err } if user == nil { user, err = c.createManagedSubscriptionUser(ctx, identity, groupInt) if err != nil { return SubscriptionAccessRef{}, err } } if err := c.updateManagedSubscriptionUser(ctx, user.ID, groupInt); err != nil { return SubscriptionAccessRef{}, err } if err := c.setManagedSubscriptionBalance(ctx, user.ID); err != nil { return SubscriptionAccessRef{}, err } if err := c.ensureManagedSubscriptionAssignment(ctx, user.ID, groupID); err != nil { return SubscriptionAccessRef{}, err } userClient, err := c.loginAsManagedSubscriptionUser(ctx, identity.Email, identity.Password) if err != nil { return SubscriptionAccessRef{}, err } keyRecord, err := c.ensureManagedSubscriptionAPIKey(ctx, userClient, user.ID, identity) if err != nil { return SubscriptionAccessRef{}, err } if err := c.bindAPIKeyGroup(ctx, keyRecord.ID, groupInt); err != nil { return SubscriptionAccessRef{}, err } return SubscriptionAccessRef{UserID: strconv.FormatInt(user.ID, 10), APIKey: identity.CustomKey}, nil } type managedSubscriptionIdentity struct { Email string Username string Password string CustomKey string KeyName string } func buildManagedSubscriptionIdentity(selector, groupID string) managedSubscriptionIdentity { normalizedSelector := strings.TrimSpace(selector) seedMaterial := strings.ToLower(normalizedSelector) + "|" + strings.TrimSpace(groupID) sum := sha256.Sum256([]byte(seedMaterial)) hash := hex.EncodeToString(sum[:]) prefix := sanitizeManagedSubscriptionPrefix(normalizedSelector) if prefix == "" { prefix = "relay-sub" } prefix = truncateManagedSubscriptionToken(prefix, 24) shortHash := hash[:16] keyHash := hash[:32] username := truncateManagedSubscriptionToken(prefix+"-"+shortHash[:8], 32) return managedSubscriptionIdentity{ Email: fmt.Sprintf("%s-%s@sub2api.local", prefix, shortHash), Username: username, Password: "RelayPwd!" + hash[:12], CustomKey: "sk-relay-" + keyHash, KeyName: truncateManagedSubscriptionToken(username+"-key", 48), } } func sanitizeManagedSubscriptionPrefix(value string) string { value = strings.ToLower(strings.TrimSpace(value)) var b strings.Builder lastDash := false for _, r := range value { switch { case r >= 'a' && r <= 'z', r >= '0' && r <= '9': b.WriteRune(r) lastDash = false case !lastDash: b.WriteByte('-') lastDash = true } } return strings.Trim(b.String(), "-") } func truncateManagedSubscriptionToken(value string, max int) string { if len(value) <= max { return value } return strings.Trim(value[:max], "-") } func (c *Client) findManagedSubscriptionUser(ctx context.Context, email string) (*adminUserRecord, error) { statusCode, _, body, err := c.perform(ctx, http.MethodGet, "/api/v1/admin/users?search="+url.QueryEscape(email)+"&page=1&page_size=20&sort_by=created_at&sort_order=desc", nil) if err != nil { return nil, fmt.Errorf("list admin users: %w", err) } if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices { return nil, newHTTPError(http.MethodGet, "/api/v1/admin/users", statusCode, body) } var envelope struct { Data struct { Items []adminUserRecord `json:"items"` } `json:"data"` } if err := json.Unmarshal(body, &envelope); err != nil { return nil, fmt.Errorf("decode admin users response: %w", err) } for _, item := range envelope.Data.Items { if strings.EqualFold(strings.TrimSpace(item.Email), email) { user := item return &user, nil } } return nil, nil } func (c *Client) createManagedSubscriptionUser(ctx context.Context, identity managedSubscriptionIdentity, groupID int64) (*adminUserRecord, error) { payload := map[string]any{ "email": identity.Email, "password": identity.Password, "username": identity.Username, "notes": "managed by sub2api-cn-relay-manager", "balance": managedSubscriptionBalance, "concurrency": 5, "allowed_groups": []int64{groupID}, } statusCode, _, body, err := c.perform(ctx, http.MethodPost, "/api/v1/admin/users", payload) if err != nil { return nil, fmt.Errorf("create admin user: %w", err) } if statusCode == http.StatusConflict { return c.findManagedSubscriptionUser(ctx, identity.Email) } if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices { return nil, newHTTPError(http.MethodPost, "/api/v1/admin/users", statusCode, body) } var user adminUserRecord if err := decodeEnvelopeObject(body, &user); err != nil { return nil, fmt.Errorf("decode created admin user: %w", err) } return &user, nil } func (c *Client) updateManagedSubscriptionUser(ctx context.Context, userID, groupID int64) error { payload := map[string]any{"allowed_groups": []int64{groupID}} statusCode, _, body, err := c.perform(ctx, http.MethodPut, fmt.Sprintf("/api/v1/admin/users/%d", userID), payload) if err != nil { return fmt.Errorf("update admin user groups: %w", err) } if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices { return newHTTPError(http.MethodPut, fmt.Sprintf("/api/v1/admin/users/%d", userID), statusCode, body) } return nil } func (c *Client) setManagedSubscriptionBalance(ctx context.Context, userID int64) error { payload := map[string]any{"balance": managedSubscriptionBalance, "operation": "set", "notes": "managed by sub2api-cn-relay-manager"} statusCode, _, body, err := c.perform(ctx, http.MethodPost, fmt.Sprintf("/api/v1/admin/users/%d/balance", userID), payload) if err != nil { return fmt.Errorf("set admin user balance: %w", err) } if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices { return newHTTPError(http.MethodPost, fmt.Sprintf("/api/v1/admin/users/%d/balance", userID), statusCode, body) } return nil } func (c *Client) ensureManagedSubscriptionAssignment(ctx context.Context, userID int64, groupID string) error { _, err := c.AssignSubscription(ctx, AssignSubscriptionRequest{ UserID: strconv.FormatInt(userID, 10), GroupID: groupID, DurationDays: managedSubscriptionValidityDays, }) if err != nil { return fmt.Errorf("assign managed subscription: %w", err) } return nil } func (c *Client) loginAsManagedSubscriptionUser(ctx context.Context, email, password string) (*Client, error) { anon := c.cloneWithAuth("", "") payload := map[string]any{"email": email, "password": password, "turnstile_token": ""} statusCode, _, body, err := anon.perform(ctx, http.MethodPost, "/api/v1/auth/login", payload) if err != nil { return nil, fmt.Errorf("login managed subscription user: %w", err) } if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices { return nil, newHTTPError(http.MethodPost, "/api/v1/auth/login", statusCode, body) } var tokenPair authTokenPair if err := decodeEnvelopeObject(body, &tokenPair); err != nil { return nil, fmt.Errorf("decode managed user login response: %w", err) } if strings.TrimSpace(tokenPair.AccessToken) == "" { return nil, fmt.Errorf("managed user login returned empty access token") } return c.cloneWithAuth("", tokenPair.AccessToken), nil } func (c *Client) ensureManagedSubscriptionAPIKey(ctx context.Context, userClient *Client, userID int64, identity managedSubscriptionIdentity) (*adminAPIKeyRecord, error) { payload := map[string]any{ "name": identity.KeyName, "custom_key": identity.CustomKey, } statusCode, _, body, err := userClient.perform(ctx, http.MethodPost, "/api/v1/keys", payload) if err != nil { return nil, fmt.Errorf("create managed api key: %w", err) } if statusCode >= http.StatusOK && statusCode < http.StatusMultipleChoices { var key adminAPIKeyRecord if err := decodeEnvelopeObject(body, &key); err != nil { return nil, fmt.Errorf("decode created api key: %w", err) } return &key, nil } if statusCode != http.StatusConflict && statusCode != http.StatusBadRequest { return nil, newHTTPError(http.MethodPost, "/api/v1/keys", statusCode, body) } return c.findManagedSubscriptionAPIKey(ctx, userID, identity) } func (c *Client) findManagedSubscriptionAPIKey(ctx context.Context, userID int64, identity managedSubscriptionIdentity) (*adminAPIKeyRecord, error) { statusCode, _, body, err := c.perform(ctx, http.MethodGet, fmt.Sprintf("/api/v1/admin/users/%d/api-keys?page=1&page_size=100&sort_by=created_at&sort_order=desc", userID), nil) if err != nil { return nil, fmt.Errorf("list managed api keys: %w", err) } if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices { return nil, newHTTPError(http.MethodGet, fmt.Sprintf("/api/v1/admin/users/%d/api-keys", userID), statusCode, body) } var envelope struct { Data struct { Items []adminAPIKeyRecord `json:"items"` } `json:"data"` } if err := json.Unmarshal(body, &envelope); err != nil { return nil, fmt.Errorf("decode admin api keys response: %w", err) } for _, item := range envelope.Data.Items { if strings.TrimSpace(item.Key) == identity.CustomKey || strings.TrimSpace(item.Name) == identity.KeyName { key := item return &key, nil } } return nil, fmt.Errorf("managed api key %q not found for user %d", identity.KeyName, userID) } func (c *Client) ensureRealSubscriptionAccess(ctx context.Context, selector string, groupID int64, probeAPIKey string) (SubscriptionAccessRef, error) { userID, err := strconv.ParseInt(strings.TrimSpace(selector), 10, 64) if err != nil { return SubscriptionAccessRef{}, fmt.Errorf("parse real subscription user id %q: %w", selector, err) } keyRecord, err := c.findUserAPIKeyByRawKey(ctx, userID, probeAPIKey) if err != nil { return SubscriptionAccessRef{}, err } if err := c.bindAPIKeyGroup(ctx, keyRecord.ID, groupID); err != nil { return SubscriptionAccessRef{}, err } return SubscriptionAccessRef{ UserID: strconv.FormatInt(userID, 10), APIKey: probeAPIKey, }, nil } func (c *Client) findUserAPIKeyByRawKey(ctx context.Context, userID int64, rawKey string) (*adminAPIKeyRecord, error) { statusCode, _, body, err := c.perform(ctx, http.MethodGet, fmt.Sprintf("/api/v1/admin/users/%d/api-keys?page=1&page_size=1000&sort_by=created_at&sort_order=desc", userID), nil) if err != nil { return nil, fmt.Errorf("list user api keys: %w", err) } if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices { return nil, newHTTPError(http.MethodGet, fmt.Sprintf("/api/v1/admin/users/%d/api-keys", userID), statusCode, body) } var envelope struct { Data struct { Items []adminAPIKeyRecord `json:"items"` } `json:"data"` } if err := json.Unmarshal(body, &envelope); err != nil { return nil, fmt.Errorf("decode user api keys response: %w", err) } trimmedRawKey := strings.TrimSpace(rawKey) for _, item := range envelope.Data.Items { if strings.TrimSpace(item.Key) == trimmedRawKey { key := item return &key, nil } } return nil, fmt.Errorf("probe api key not found for user %d", userID) } func (c *Client) bindAPIKeyGroup(ctx context.Context, keyID, groupID int64) error { payload := map[string]any{"group_id": groupID} statusCode, _, body, err := c.perform(ctx, http.MethodPut, fmt.Sprintf("/api/v1/admin/api-keys/%d", keyID), payload) if err != nil { return fmt.Errorf("bind api key group: %w", err) } if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices { return newHTTPError(http.MethodPut, fmt.Sprintf("/api/v1/admin/api-keys/%d", keyID), statusCode, body) } return nil } func (c *Client) cloneWithAuth(apiKey, bearerToken string) *Client { if c == nil { return nil } clone := *c clone.apiKey = strings.TrimSpace(apiKey) clone.bearerToken = strings.TrimSpace(bearerToken) return &clone }