feat(api): structured provider credentials + trial-auth validation on account create

POST /accounts now accepts secret as a provider-specific JSON object
instead of an opaque string, and validates credentials via
provider.Provider.Validate before persisting — invalid credentials get
a generic 400 without ever reaching Store.CreateAccount or echoing the
secret back.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
This commit is contained in:
2026-07-04 20:12:41 +07:00
parent 32107571d1
commit 568452846a
3 changed files with 66 additions and 13 deletions
+13 -2
View File
@@ -31,11 +31,22 @@ func (a *API) handleCreateAccount(w http.ResponseWriter, r *http.Request) {
if !decodeBody(w, r, &req) {
return
}
if req.Provider == "" || req.Secret == "" {
if req.Provider == "" || len(req.Secret) == 0 {
writeErr(w, http.StatusBadRequest, "provider and secret are required")
return
}
secretEnc, err := a.Cipher.Encrypt([]byte(req.Secret))
p, err := a.Reg.ByName(req.Provider)
if err != nil {
writeErr(w, http.StatusBadRequest, "unknown provider")
return
}
// Trial auth up-front so bad credentials fail at creation, not at import.
// The error text is deliberately generic — never echo the secret back.
if err := p.Validate(r.Context(), provider.Credentials{Secret: string(req.Secret)}); err != nil {
writeErr(w, http.StatusBadRequest, "invalid provider credentials")
return
}
secretEnc, err := a.Cipher.Encrypt(req.Secret)
if err != nil {
log.Printf("api: encrypt secret failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")