93 lines
3.2 KiB
Go
93 lines
3.2 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/wangjia/pangolin/server/internal/apierr"
|
|
"github.com/wangjia/pangolin/server/internal/auth"
|
|
"github.com/wangjia/pangolin/server/internal/routing"
|
|
)
|
|
|
|
// RoutingAPI serves /v1/me/routing: the user's configurable routing profile
|
|
// (可配置分流). GET returns the stored profile or routing.Default() when the
|
|
// user hasn't customized one yet; POST validates and upserts.
|
|
type RoutingAPI struct {
|
|
store *routing.Store
|
|
// lockedDomains are the system-forced-tunnel private-service domains
|
|
// (PANGOLIN_PRIVATE_SPLIT_DOMAINS, same slice injected into NodeAPI) —
|
|
// read-only, surfaced to GET so clients can warn users that rules against
|
|
// these domains silently have no effect. Never written to the persisted
|
|
// Profile.
|
|
lockedDomains []string
|
|
}
|
|
|
|
// NewRoutingAPI creates a RoutingAPI backed by the given routing.Store.
|
|
// lockedDomains is the system-forced private-service domain list (may be nil).
|
|
func NewRoutingAPI(store *routing.Store, lockedDomains []string) *RoutingAPI {
|
|
return &RoutingAPI{store: store, lockedDomains: lockedDomains}
|
|
}
|
|
|
|
// profileResponse wraps routing.Profile for GET /v1/me/routing, adding the
|
|
// read-only system_locked_domains list. It deliberately lives here — not on
|
|
// routing.Profile itself — so the field can never leak into the writable
|
|
// Profile contract that SaveProfile decodes POST bodies into.
|
|
type profileResponse struct {
|
|
*routing.Profile
|
|
SystemLockedDomains []string `json:"system_locked_domains"`
|
|
}
|
|
|
|
// GetProfile handles GET /v1/me/routing.
|
|
func (a *RoutingAPI) GetProfile(w http.ResponseWriter, r *http.Request) {
|
|
uid, ok := auth.UserIDFromContext(r.Context())
|
|
if !ok {
|
|
apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized)
|
|
return
|
|
}
|
|
p, err := a.store.Get(r.Context(), uid)
|
|
if err != nil {
|
|
apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
|
|
return
|
|
}
|
|
if p == nil {
|
|
p = routing.Default()
|
|
}
|
|
locked := a.lockedDomains
|
|
if locked == nil {
|
|
locked = []string{}
|
|
}
|
|
writeJSON(w, http.StatusOK, profileResponse{Profile: p, SystemLockedDomains: locked})
|
|
}
|
|
|
|
// SaveProfile handles POST /v1/me/routing. On validation failure it returns
|
|
// 400 with every offending field reported at once (no partial save).
|
|
func (a *RoutingAPI) SaveProfile(w http.ResponseWriter, r *http.Request) {
|
|
uid, ok := auth.UserIDFromContext(r.Context())
|
|
if !ok {
|
|
apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized)
|
|
return
|
|
}
|
|
var p routing.Profile
|
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64*1024)).Decode(&p); err != nil {
|
|
apierr.WriteJSON(w, http.StatusBadRequest, apierr.ErrBadRequest)
|
|
return
|
|
}
|
|
p.Normalize()
|
|
if errs := p.Validate(); len(errs) > 0 {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"code": "routing_invalid",
|
|
"message_zh": "规则校验未通过",
|
|
"message_en": "Rule validation failed",
|
|
"errors": errs,
|
|
})
|
|
return
|
|
}
|
|
if err := a.store.Upsert(r.Context(), uid, &p); err != nil {
|
|
apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, &p)
|
|
}
|