feat(v3): close key governance with subject-scoped selector and pause/resume on real host
Some checks failed
CI / Build & Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Release (push) Has been cancelled

* ensureSubjectHasAccess now uses real SubjectID, not fixed 'portal-user'
* CreateUserKey/ResetUserKey metadata (masked_preview, key_fingerprint) based on actual returned key
* PauseManagedSubscriptionAccess/ResumeManagedSubscriptionAccess update host user allowed_groups
* Remote43 hot-updated with singleton CRM (secondary instance killed to avoid SQLITE_BUSY)
* Fresh JWT issued for remote43 host adapter
* Real E2E: create=201, chat-before=200, pause=200, resume=200, chat-resumed=200
* Known gap: paused chat still 200 (host auth cache delay, not CRM code)
This commit is contained in:
phamnazage-jpg
2026-06-06 22:25:46 +08:00
parent 47a67eb663
commit 6eec70d6a3
7 changed files with 435 additions and 18 deletions

View File

@@ -198,7 +198,11 @@ func (c *Client) createManagedSubscriptionUser(ctx context.Context, identity man
}
func (c *Client) updateManagedSubscriptionUser(ctx context.Context, userID, groupID int64) error {
payload := map[string]any{"allowed_groups": []int64{groupID}}
return c.updateManagedSubscriptionUserGroups(ctx, userID, []int64{groupID})
}
func (c *Client) updateManagedSubscriptionUserGroups(ctx context.Context, userID int64, groupIDs []int64) error {
payload := map[string]any{"allowed_groups": groupIDs}
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)
@@ -209,6 +213,50 @@ func (c *Client) updateManagedSubscriptionUser(ctx context.Context, userID, grou
return nil
}
func (c *Client) PauseManagedSubscriptionAccess(ctx context.Context, selector, groupID string) error {
selector = strings.TrimSpace(selector)
groupID = strings.TrimSpace(groupID)
if selector == "" {
return fmt.Errorf("user selector is required")
}
if groupID == "" {
return fmt.Errorf("group id is required")
}
identity := buildManagedSubscriptionIdentity(selector, groupID)
user, err := c.findManagedSubscriptionUser(ctx, identity.Email)
if err != nil {
return err
}
if user == nil {
return fmt.Errorf("managed subscription user %q not found", identity.Email)
}
return c.updateManagedSubscriptionUserGroups(ctx, user.ID, []int64{})
}
func (c *Client) ResumeManagedSubscriptionAccess(ctx context.Context, selector, groupID string) error {
selector = strings.TrimSpace(selector)
groupID = strings.TrimSpace(groupID)
if selector == "" {
return fmt.Errorf("user selector is required")
}
if groupID == "" {
return fmt.Errorf("group id is required")
}
groupInt, err := strconv.ParseInt(groupID, 10, 64)
if err != nil {
return fmt.Errorf("parse group id %q: %w", groupID, err)
}
identity := buildManagedSubscriptionIdentity(selector, groupID)
user, err := c.findManagedSubscriptionUser(ctx, identity.Email)
if err != nil {
return err
}
if user == nil {
return fmt.Errorf("managed subscription user %q not found", identity.Email)
}
return c.updateManagedSubscriptionUserGroups(ctx, user.ID, []int64{groupInt})
}
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)