367 lines
14 KiB
Go
367 lines
14 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
|
|
LogicalGroupID string
|
|
RouteID string
|
|
ShadowGroupID string
|
|
AccountStatus string
|
|
BindingState string
|
|
Query string
|
|
Limit int
|
|
}
|
|
|
|
type UpdateProviderAccountStatusRequest struct {
|
|
AccountID int64 `json:"-"`
|
|
AccountStatus string `json:"-"`
|
|
DisabledReason string `json:"reason,omitempty"`
|
|
}
|
|
|
|
type GetProviderAccountBindingCandidatesRequest struct {
|
|
AccountID int64
|
|
}
|
|
|
|
type UpdateProviderAccountBindingRequest struct {
|
|
AccountID int64 `json:"-"`
|
|
RouteID string `json:"route_id,omitempty"`
|
|
Clear bool `json:"clear,omitempty"`
|
|
}
|
|
|
|
type ProviderAccountBindingCandidatesResult struct {
|
|
ProviderAccount ProviderAccountInfo `json:"provider_account"`
|
|
CandidateRoutes []LogicalGroupRouteInfo `json:"candidate_routes"`
|
|
}
|
|
|
|
type ProviderAccountInfo struct {
|
|
ID int64 `json:"id"`
|
|
HostID string `json:"host_id"`
|
|
HostBaseURL string `json:"host_base_url"`
|
|
ProviderID string `json:"provider_id"`
|
|
ProviderName string `json:"provider_name"`
|
|
RouteName string `json:"route_name,omitempty"`
|
|
RouteID string `json:"route_id,omitempty"`
|
|
LogicalGroupID string `json:"logical_group_id,omitempty"`
|
|
ShadowGroupID string `json:"shadow_group_id,omitempty"`
|
|
ShadowHostID string `json:"shadow_host_id,omitempty"`
|
|
UpstreamBaseURLHint string `json:"upstream_base_url_hint,omitempty"`
|
|
HostAccountID string `json:"host_account_id"`
|
|
KeyFingerprint string `json:"key_fingerprint"`
|
|
AccountName string `json:"account_name"`
|
|
AccountStatus string `json:"account_status"`
|
|
BindingState string `json:"binding_state,omitempty"`
|
|
BindingCandidateCount int `json:"binding_candidate_count,omitempty"`
|
|
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")),
|
|
LogicalGroupID: strings.TrimSpace(r.URL.Query().Get("logical_group_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")),
|
|
BindingState: strings.TrimSpace(r.URL.Query().Get("binding_state")),
|
|
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 handleGetProviderAccountBindingCandidates(w http.ResponseWriter, r *http.Request, fn func(context.Context, GetProviderAccountBindingCandidatesRequest) (ProviderAccountBindingCandidatesResult, error)) {
|
|
if fn == nil {
|
|
writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "get-provider-account-binding-candidates action is not configured"})
|
|
return
|
|
}
|
|
accountID, parseErr := parseProviderAccountID(r.PathValue("accountID"))
|
|
if parseErr != nil {
|
|
writeHTTPError(w, parseErr)
|
|
return
|
|
}
|
|
result, actionErr := fn(r.Context(), GetProviderAccountBindingCandidatesRequest{AccountID: accountID})
|
|
if actionErr != nil {
|
|
writeHTTPError(w, classifyError(actionErr))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
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 handleUpdateProviderAccountBinding(w http.ResponseWriter, r *http.Request, fn func(context.Context, UpdateProviderAccountBindingRequest) (ProviderAccountInfo, error)) {
|
|
if fn == nil {
|
|
writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "update-provider-account-binding action is not configured"})
|
|
return
|
|
}
|
|
accountID, err := parseProviderAccountID(r.PathValue("accountID"))
|
|
if err != nil {
|
|
writeHTTPError(w, err)
|
|
return
|
|
}
|
|
var req UpdateProviderAccountBindingRequest
|
|
if r.ContentLength != 0 {
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
writeHTTPError(w, err)
|
|
return
|
|
}
|
|
}
|
|
req.AccountID = accountID
|
|
account, actionErr := fn(r.Context(), req)
|
|
if actionErr != nil {
|
|
writeHTTPError(w, classifyError(actionErr))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"provider_account": account})
|
|
}
|
|
|
|
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
|
|
}
|
|
accountID, err := parseProviderAccountID(r.PathValue("accountID"))
|
|
if err != nil {
|
|
writeHTTPError(w, err)
|
|
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, actionErr := fn(r.Context(), req)
|
|
if actionErr != nil {
|
|
writeHTTPError(w, classifyError(actionErr))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"provider_account": account})
|
|
}
|
|
|
|
func parseProviderAccountID(rawID string) (int64, *httpError) {
|
|
accountID, err := strconv.ParseInt(strings.TrimSpace(rawID), 10, 64)
|
|
if err != nil || accountID <= 0 {
|
|
return 0, &httpError{StatusCode: http.StatusBadRequest, Code: "invalid_request", Message: "account_id must be a positive integer"}
|
|
}
|
|
return accountID, nil
|
|
}
|
|
|
|
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,
|
|
LogicalGroupID: req.LogicalGroupID,
|
|
RouteID: req.RouteID,
|
|
ShadowGroupID: req.ShadowGroupID,
|
|
AccountStatus: req.AccountStatus,
|
|
BindingState: req.BindingState,
|
|
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 buildGetProviderAccountBindingCandidatesAction(sqliteDSN string) func(context.Context, GetProviderAccountBindingCandidatesRequest) (ProviderAccountBindingCandidatesResult, error) {
|
|
return func(ctx context.Context, req GetProviderAccountBindingCandidatesRequest) (ProviderAccountBindingCandidatesResult, error) {
|
|
store, err := sqlite.Open(ctx, sqliteDSN)
|
|
if err != nil {
|
|
return ProviderAccountBindingCandidatesResult{}, err
|
|
}
|
|
defer store.Close()
|
|
|
|
account, err := store.ProviderAccounts().GetViewByID(ctx, req.AccountID)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return ProviderAccountBindingCandidatesResult{}, fmt.Errorf("provider account %d not found", req.AccountID)
|
|
}
|
|
return ProviderAccountBindingCandidatesResult{}, err
|
|
}
|
|
candidates := make([]LogicalGroupRouteInfo, 0)
|
|
if strings.TrimSpace(account.HostID) != "" && strings.TrimSpace(account.ShadowGroupID) != "" {
|
|
routes, routeErr := store.LogicalGroupRoutes().ListByShadowBinding(ctx, account.HostID, account.ShadowGroupID)
|
|
if routeErr != nil {
|
|
return ProviderAccountBindingCandidatesResult{}, routeErr
|
|
}
|
|
for _, route := range routes {
|
|
candidates = append(candidates, logicalGroupRouteRowToInfo(route, nil))
|
|
}
|
|
}
|
|
return ProviderAccountBindingCandidatesResult{
|
|
ProviderAccount: providerAccountViewToInfo(account),
|
|
CandidateRoutes: candidates,
|
|
}, 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 buildUpdateProviderAccountBindingAction(sqliteDSN string) func(context.Context, UpdateProviderAccountBindingRequest) (ProviderAccountInfo, error) {
|
|
return func(ctx context.Context, req UpdateProviderAccountBindingRequest) (ProviderAccountInfo, error) {
|
|
store, err := sqlite.Open(ctx, sqliteDSN)
|
|
if err != nil {
|
|
return ProviderAccountInfo{}, err
|
|
}
|
|
defer store.Close()
|
|
|
|
account, err := store.ProviderAccounts().GetByID(ctx, req.AccountID)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return ProviderAccountInfo{}, fmt.Errorf("provider account %d not found", req.AccountID)
|
|
}
|
|
return ProviderAccountInfo{}, err
|
|
}
|
|
|
|
if req.Clear {
|
|
if err := store.ProviderAccounts().UpdateBindingByID(ctx, account.ID, "", account.ShadowGroupID); err != nil {
|
|
return ProviderAccountInfo{}, err
|
|
}
|
|
} else {
|
|
routeID := strings.TrimSpace(req.RouteID)
|
|
if routeID == "" {
|
|
return ProviderAccountInfo{}, fmt.Errorf("route_id is required")
|
|
}
|
|
route, routeErr := store.LogicalGroupRoutes().GetByRouteID(ctx, routeID)
|
|
if routeErr != nil {
|
|
if routeErr == sql.ErrNoRows {
|
|
return ProviderAccountInfo{}, fmt.Errorf("logical group route %q not found", routeID)
|
|
}
|
|
return ProviderAccountInfo{}, routeErr
|
|
}
|
|
if strings.TrimSpace(route.ShadowHostID) != strings.TrimSpace(accountHostIDForBinding(store, ctx, account)) {
|
|
return ProviderAccountInfo{}, fmt.Errorf("route %q shadow_host_id does not match provider account host", routeID)
|
|
}
|
|
if strings.TrimSpace(account.ShadowGroupID) != "" && strings.TrimSpace(route.ShadowGroupID) != strings.TrimSpace(account.ShadowGroupID) {
|
|
return ProviderAccountInfo{}, fmt.Errorf("route %q shadow_group_id does not match provider account shadow_group_id", routeID)
|
|
}
|
|
if err := store.ProviderAccounts().UpdateBindingByID(ctx, account.ID, route.RouteID, route.ShadowGroupID); err != nil {
|
|
return ProviderAccountInfo{}, err
|
|
}
|
|
}
|
|
|
|
updated, err := store.ProviderAccounts().GetViewByID(ctx, account.ID)
|
|
if err != nil {
|
|
return ProviderAccountInfo{}, err
|
|
}
|
|
return providerAccountViewToInfo(updated), nil
|
|
}
|
|
}
|
|
|
|
func accountHostIDForBinding(store *sqlite.DB, ctx context.Context, account sqlite.ProviderAccount) string {
|
|
if store == nil {
|
|
return ""
|
|
}
|
|
hostRow, err := store.Hosts().GetByID(ctx, account.HostID)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(hostRow.HostID)
|
|
}
|
|
|
|
func providerAccountViewToInfo(row sqlite.ProviderAccountView) ProviderAccountInfo {
|
|
return ProviderAccountInfo{
|
|
ID: row.ID,
|
|
HostID: row.HostID,
|
|
HostBaseURL: row.HostBaseURL,
|
|
ProviderID: row.ProviderID,
|
|
ProviderName: row.ProviderName,
|
|
RouteName: row.RouteName,
|
|
RouteID: row.RouteID,
|
|
LogicalGroupID: row.LogicalGroupID,
|
|
ShadowGroupID: row.ShadowGroupID,
|
|
ShadowHostID: row.ShadowHostID,
|
|
UpstreamBaseURLHint: row.UpstreamBaseURLHint,
|
|
HostAccountID: row.HostAccountID,
|
|
KeyFingerprint: row.KeyFingerprint,
|
|
AccountName: row.AccountName,
|
|
AccountStatus: row.AccountStatus,
|
|
BindingState: row.BindingState,
|
|
BindingCandidateCount: row.BindingCandidateCount,
|
|
LastProbeStatus: row.LastProbeStatus,
|
|
LastProbeAt: row.LastProbeAt,
|
|
DisabledReason: row.DisabledReason,
|
|
CreatedAt: row.CreatedAt,
|
|
UpdatedAt: row.UpdatedAt,
|
|
}
|
|
}
|