Files
sub2api-cn-relay-manager/internal/app/provider_accounts_api.go
2026-05-29 14:43:34 +08:00

189 lines
6.8 KiB
Go

package app
import (
"context"
"database/sql"
"fmt"
"net/http"
"strconv"
"strings"
"sub2api-cn-relay-manager/internal/store/sqlite"
)
type ListProviderAccountsRequest struct {
HostID string
ProviderID string
RouteID string
ShadowGroupID string
AccountStatus string
Query string
Limit int
}
type UpdateProviderAccountStatusRequest struct {
AccountID int64 `json:"-"`
AccountStatus string `json:"-"`
DisabledReason string `json:"reason,omitempty"`
}
type ProviderAccountInfo struct {
ID int64 `json:"id"`
HostID string `json:"host_id"`
ProviderID string `json:"provider_id"`
ProviderName string `json:"provider_name"`
RouteID string `json:"route_id,omitempty"`
LogicalGroupID string `json:"logical_group_id,omitempty"`
ShadowGroupID string `json:"shadow_group_id,omitempty"`
HostAccountID string `json:"host_account_id"`
KeyFingerprint string `json:"key_fingerprint"`
AccountName string `json:"account_name"`
AccountStatus string `json:"account_status"`
LastProbeStatus string `json:"last_probe_status,omitempty"`
LastProbeAt string `json:"last_probe_at,omitempty"`
DisabledReason string `json:"disabled_reason,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
}
func handleListProviderAccounts(w http.ResponseWriter, r *http.Request, fn func(context.Context, ListProviderAccountsRequest) ([]ProviderAccountInfo, error)) {
if fn == nil {
writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "list-provider-accounts action is not configured"})
return
}
accounts, err := fn(r.Context(), ListProviderAccountsRequest{
HostID: strings.TrimSpace(r.URL.Query().Get("host_id")),
ProviderID: strings.TrimSpace(r.URL.Query().Get("provider_id")),
RouteID: strings.TrimSpace(r.URL.Query().Get("route_id")),
ShadowGroupID: strings.TrimSpace(r.URL.Query().Get("shadow_group_id")),
AccountStatus: strings.TrimSpace(r.URL.Query().Get("account_status")),
Query: strings.TrimSpace(r.URL.Query().Get("q")),
Limit: parsePositiveInt(r.URL.Query().Get("limit")),
})
if err != nil {
writeHTTPError(w, classifyError(err))
return
}
if accounts == nil {
accounts = []ProviderAccountInfo{}
}
writeJSON(w, http.StatusOK, map[string]any{"provider_accounts": accounts})
}
func handleEnableProviderAccount(w http.ResponseWriter, r *http.Request, fn func(context.Context, UpdateProviderAccountStatusRequest) (ProviderAccountInfo, error)) {
handleUpdateProviderAccountStatus(w, r, fn, sqlite.ProviderAccountStatusActive)
}
func handleDisableProviderAccount(w http.ResponseWriter, r *http.Request, fn func(context.Context, UpdateProviderAccountStatusRequest) (ProviderAccountInfo, error)) {
handleUpdateProviderAccountStatus(w, r, fn, sqlite.ProviderAccountStatusDisabled)
}
func handleRetireProviderAccount(w http.ResponseWriter, r *http.Request, fn func(context.Context, UpdateProviderAccountStatusRequest) (ProviderAccountInfo, error)) {
handleUpdateProviderAccountStatus(w, r, fn, sqlite.ProviderAccountStatusDeprecated)
}
func handleUpdateProviderAccountStatus(w http.ResponseWriter, r *http.Request, fn func(context.Context, UpdateProviderAccountStatusRequest) (ProviderAccountInfo, error), accountStatus string) {
if fn == nil {
writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "update-provider-account-status action is not configured"})
return
}
rawID := strings.TrimSpace(r.PathValue("accountID"))
accountID, err := strconv.ParseInt(rawID, 10, 64)
if err != nil || accountID <= 0 {
writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "invalid_request", Message: "account_id must be a positive integer"})
return
}
req := UpdateProviderAccountStatusRequest{
AccountID: accountID,
AccountStatus: accountStatus,
}
if r.ContentLength != 0 {
if err := decodeJSON(r, &req); err != nil {
writeHTTPError(w, err)
return
}
req.AccountID = accountID
req.AccountStatus = accountStatus
}
account, err := fn(r.Context(), req)
if err != nil {
writeHTTPError(w, classifyError(err))
return
}
writeJSON(w, http.StatusOK, map[string]any{"provider_account": account})
}
func buildListProviderAccountsAction(sqliteDSN string) func(context.Context, ListProviderAccountsRequest) ([]ProviderAccountInfo, error) {
return func(ctx context.Context, req ListProviderAccountsRequest) ([]ProviderAccountInfo, error) {
store, err := sqlite.Open(ctx, sqliteDSN)
if err != nil {
return nil, err
}
defer store.Close()
if err := sqlite.SyncProviderAccountsFromLatestImportBatches(ctx, store); err != nil {
return nil, err
}
rows, err := store.ProviderAccounts().List(ctx, sqlite.ProviderAccountListFilter{
HostID: req.HostID,
ProviderID: req.ProviderID,
RouteID: req.RouteID,
ShadowGroupID: req.ShadowGroupID,
AccountStatus: req.AccountStatus,
Query: req.Query,
Limit: req.Limit,
})
if err != nil {
return nil, err
}
result := make([]ProviderAccountInfo, 0, len(rows))
for _, row := range rows {
result = append(result, providerAccountViewToInfo(row))
}
return result, nil
}
}
func buildUpdateProviderAccountStatusAction(sqliteDSN, accountStatus string) func(context.Context, UpdateProviderAccountStatusRequest) (ProviderAccountInfo, error) {
return func(ctx context.Context, req UpdateProviderAccountStatusRequest) (ProviderAccountInfo, error) {
store, err := sqlite.Open(ctx, sqliteDSN)
if err != nil {
return ProviderAccountInfo{}, err
}
defer store.Close()
if err := store.ProviderAccounts().UpdateStatusByID(ctx, req.AccountID, accountStatus, strings.TrimSpace(req.DisabledReason)); err != nil {
if err == sql.ErrNoRows {
return ProviderAccountInfo{}, fmt.Errorf("provider account %d not found", req.AccountID)
}
return ProviderAccountInfo{}, err
}
updated, err := store.ProviderAccounts().GetViewByID(ctx, req.AccountID)
if err != nil {
return ProviderAccountInfo{}, err
}
return providerAccountViewToInfo(updated), nil
}
}
func providerAccountViewToInfo(row sqlite.ProviderAccountView) ProviderAccountInfo {
return ProviderAccountInfo{
ID: row.ID,
HostID: row.HostID,
ProviderID: row.ProviderID,
ProviderName: row.ProviderName,
RouteID: row.RouteID,
LogicalGroupID: row.LogicalGroupID,
ShadowGroupID: row.ShadowGroupID,
HostAccountID: row.HostAccountID,
KeyFingerprint: row.KeyFingerprint,
AccountName: row.AccountName,
AccountStatus: row.AccountStatus,
LastProbeStatus: row.LastProbeStatus,
LastProbeAt: row.LastProbeAt,
DisabledReason: row.DisabledReason,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}