package handler import ( "encoding/json" "errors" "net/http" "regexp" "strings" "github.com/baicai2026-baicai/goods/api/internal/store" ) // emailRe is a deliberately permissive sanity check; real validation is the // unique constraint plus the user being able to receive their own key. var emailRe = regexp.MustCompile(`^[^@\s]+@[^@\s]+\.[^@\s]+$`) const minPasswordLen = 8 type credentials struct { Email string `json:"email"` Password string `json:"password"` } func decodeCredentials(w http.ResponseWriter, r *http.Request) (credentials, bool) { var c credentials if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&c); err != nil { writeError(w, r, http.StatusBadRequest, "invalid_body", "请求格式无效") return credentials{}, false } c.Email = strings.TrimSpace(c.Email) if !emailRe.MatchString(c.Email) { writeError(w, r, http.StatusBadRequest, "invalid_email", "邮箱格式无效") return credentials{}, false } if len(c.Password) < minPasswordLen { writeError(w, r, http.StatusBadRequest, "weak_password", "密码至少需要 8 位") return credentials{}, false } return c, true } // keyResponse is returned whenever a fresh plaintext key is issued; the key is // shown exactly once and cannot be recovered afterwards. type keyResponse struct { Email string `json:"email"` APIKey string `json:"api_key"` KeyPrefix string `json:"key_prefix"` RateLimitPerMin int `json:"rate_limit_per_min"` QuotaTotal int64 `json:"quota_total"` } // Register creates an account and issues its first API key. POST {email, password}. func (h *Handler) Register(w http.ResponseWriter, r *http.Request) { c, ok := decodeCredentials(w, r) if !ok { return } key, acct, err := h.store.RegisterUser(r.Context(), c.Email, c.Password, h.regRatePerMin, h.regQuotaTotal) if errors.Is(err, store.ErrEmailTaken) { writeError(w, r, http.StatusConflict, "email_taken", "该邮箱已注册,请直接登录查看或重置密钥") return } if h.handleErr(w, r, err) { return } writeJSON(w, http.StatusCreated, keyResponse{ Email: acct.Email, APIKey: key, KeyPrefix: acct.KeyPrefix, RateLimitPerMin: acct.RateLimitPerMin, QuotaTotal: acct.QuotaTotal, }) } // AccountInfo verifies credentials and returns the account's key metadata plus // cumulative usage. POST {email, password}. The plaintext key is not returned. func (h *Handler) AccountInfo(w http.ResponseWriter, r *http.Request) { c, ok := decodeCredentials(w, r) if !ok { return } acct, err := h.store.Authenticate(r.Context(), c.Email, c.Password) if errors.Is(err, store.ErrNotFound) { writeError(w, r, http.StatusUnauthorized, "invalid_credentials", "邮箱或密码错误") return } if h.handleErr(w, r, err) { return } used := h.limiter.TotalUsed(r.Context(), acct.KeyID) remaining := acct.QuotaTotal - used if remaining < 0 { remaining = 0 } writeJSON(w, http.StatusOK, map[string]any{ "email": acct.Email, "key_prefix": acct.KeyPrefix, "rate_limit_per_min": acct.RateLimitPerMin, "quota_total": acct.QuotaTotal, "quota_used": used, "quota_remaining": remaining, }) } // RegenerateKey revokes the account's current key and issues a new one, carrying // over cumulative usage so the quota cannot be reset. POST {email, password}. func (h *Handler) RegenerateKey(w http.ResponseWriter, r *http.Request) { c, ok := decodeCredentials(w, r) if !ok { return } key, acct, oldKeyID, err := h.store.RegenerateKey(r.Context(), c.Email, c.Password, h.regRatePerMin, h.regQuotaTotal) if errors.Is(err, store.ErrNotFound) { writeError(w, r, http.StatusUnauthorized, "invalid_credentials", "邮箱或密码错误") return } if h.handleErr(w, r, err) { return } h.limiter.CopyTotal(r.Context(), oldKeyID, acct.KeyID) writeJSON(w, http.StatusOK, keyResponse{ Email: acct.Email, APIKey: key, KeyPrefix: acct.KeyPrefix, RateLimitPerMin: acct.RateLimitPerMin, QuotaTotal: acct.QuotaTotal, }) }