The add and remove buttons, and the read that checks them
Anker documents neither rfid write, so the bodies are inferred from the field names get_device_cards answers with, and every write re-reads the list: what the card shows is what the account holds, never what an undocumented endpoint claimed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a5842201c0
commit
245870a96a
@@ -584,3 +584,100 @@ func (s *Server) handleAnkerChargerDetails(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
writeJSON(w, http.StatusOK, json.RawMessage(raw))
|
||||
}
|
||||
|
||||
// The two card writes. They are the only calls in the Anker connector that
|
||||
// change anything on the account, so both are gated exactly like the reads,
|
||||
// rate-limited beside the control commands — a card is who may start a charge,
|
||||
// which is the same actuator asked a slower question — and audited by serial and
|
||||
// card, with the number kept out of the log line: it is the credential itself.
|
||||
// Both answer with the card list as it stands after the write, so the caller
|
||||
// sees what the account holds rather than what an undocumented endpoint claimed.
|
||||
|
||||
// ankerCardBody is what a card write is asked for.
|
||||
type ankerCardBody struct {
|
||||
CardNumber string `json:"cardNumber"`
|
||||
CardName string `json:"cardName"`
|
||||
}
|
||||
|
||||
// POST /api/integrations/anker-solix/chargers/{sn}/rfid-cards — add a card to
|
||||
// the charger, or rename one already on it.
|
||||
func (s *Server) handleAnkerCardSave(w http.ResponseWriter, r *http.Request) {
|
||||
who, sn, cfg, ok := s.ankerCardGate(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var body ankerCardBody
|
||||
if r.Body != nil {
|
||||
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||
}
|
||||
if strings.TrimSpace(body.CardNumber) == "" {
|
||||
writeError(w, http.StatusBadRequest, "card number required")
|
||||
return
|
||||
}
|
||||
s.ankerCardWrite(w, r, who, cfg, "rfid-card-save", sn, map[string]any{
|
||||
"sn": sn, "cardNumber": body.CardNumber, "cardName": body.CardName,
|
||||
})
|
||||
}
|
||||
|
||||
// DELETE /api/integrations/anker-solix/chargers/{sn}/rfid-cards/{number} —
|
||||
// remove one card, named in full.
|
||||
func (s *Server) handleAnkerCardDelete(w http.ResponseWriter, r *http.Request) {
|
||||
who, sn, cfg, ok := s.ankerCardGate(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
number := strings.TrimSpace(r.PathValue("number"))
|
||||
if number == "" {
|
||||
writeError(w, http.StatusBadRequest, "card number required")
|
||||
return
|
||||
}
|
||||
s.ankerCardWrite(w, r, who, cfg, "rfid-card-delete", sn, map[string]any{
|
||||
"sn": sn, "cardNumber": number,
|
||||
})
|
||||
}
|
||||
|
||||
// ankerCardGate is everything both writes need before they may run: a caller, a
|
||||
// serial, an integration that is on and has credentials, and a rate limit. A
|
||||
// gate that is off answers 409 rather than the reads' 200-with-a-reason: a write
|
||||
// that did not happen is not a state to render, it is a request that failed.
|
||||
func (s *Server) ankerCardGate(w http.ResponseWriter, r *http.Request) (*callerIdentity, string, map[string]string, bool) {
|
||||
who := caller(r)
|
||||
if who == nil {
|
||||
writeError(w, http.StatusUnauthorized, "not authenticated")
|
||||
return nil, "", nil, false
|
||||
}
|
||||
sn := strings.TrimSpace(r.PathValue("sn"))
|
||||
if sn == "" {
|
||||
writeError(w, http.StatusBadRequest, "charger serial required")
|
||||
return nil, "", nil, false
|
||||
}
|
||||
userRaw := s.userPluginSettings(r.Context(), who.ID)
|
||||
res := s.resolveAnker(r.Context(), who, userRaw)
|
||||
if reason := ankerGate(res, true); reason != "" {
|
||||
writeError(w, http.StatusConflict, reason)
|
||||
return nil, "", nil, false
|
||||
}
|
||||
if !s.ctlRL.allow(who.ID + "|" + sn) {
|
||||
writeError(w, http.StatusTooManyRequests, "too many card changes; please slow down")
|
||||
return nil, "", nil, false
|
||||
}
|
||||
return who, sn, map[string]string{
|
||||
"email": res.eff.Email,
|
||||
"password": res.eff.Password,
|
||||
"country": res.eff.Country,
|
||||
}, true
|
||||
}
|
||||
|
||||
// ankerCardWrite runs one card capability and relays its document. The audit
|
||||
// line names the charger and how the write went, never the card number.
|
||||
func (s *Server) ankerCardWrite(w http.ResponseWriter, r *http.Request, who *callerIdentity,
|
||||
cfg map[string]string, action, sn string, params map[string]any) {
|
||||
raw, err := s.plugins.InvokeWith(r.Context(), ankerPlugin, cfg, action, mustJSON(params))
|
||||
if err != nil {
|
||||
s.auditControl(who, sn, action, nil, "failed", err)
|
||||
writeJSON(w, http.StatusBadGateway, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
s.auditControl(who, sn, action, nil, "ok", nil)
|
||||
writeJSON(w, http.StatusOK, json.RawMessage(raw))
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@
|
||||
// POST /api/integrations/anker-solix/health
|
||||
// GET /api/integrations/anker-solix/chargers
|
||||
// GET /api/integrations/anker-solix/chargers/{sn}/details
|
||||
// POST /api/integrations/anker-solix/chargers/{sn}/rfid-cards
|
||||
// DELETE /api/integrations/anker-solix/chargers/{sn}/rfid-cards/{number}
|
||||
// GET /api/integrations/greencell PUT /api/integrations/greencell
|
||||
// POST /api/integrations/greencell/health
|
||||
// GET /api/integrations/greencell/chargers
|
||||
@@ -441,6 +443,8 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("POST /api/integrations/anker-solix/health", s.handleAnkerHealth)
|
||||
mux.HandleFunc("GET /api/integrations/anker-solix/chargers", s.handleAnkerChargers)
|
||||
mux.HandleFunc("GET /api/integrations/anker-solix/chargers/{sn}/details", s.handleAnkerChargerDetails)
|
||||
mux.HandleFunc("POST /api/integrations/anker-solix/chargers/{sn}/rfid-cards", s.handleAnkerCardSave)
|
||||
mux.HandleFunc("DELETE /api/integrations/anker-solix/chargers/{sn}/rfid-cards/{number}", s.handleAnkerCardDelete)
|
||||
mux.HandleFunc("GET /api/integrations/greencell", s.handleGetGreencell)
|
||||
mux.HandleFunc("PUT /api/integrations/greencell", s.handlePutGreencell)
|
||||
mux.HandleFunc("POST /api/integrations/greencell/health", s.handleGreencellHealth)
|
||||
|
||||
Reference in New Issue
Block a user