package handlers import ( "bytes" "encoding/json" "fmt" "io" "log" "net" "net/http" "strconv" "strings" ) func (a *App) handleResendReset(w http.ResponseWriter, r *http.Request) { username := r.FormValue("username") if username == "" { if email := r.FormValue("email"); email != "" { username = sanitizeUsername(email) } } if username == "" { respondResendError(w, http.StatusBadRequest, "email or username required", 0) return } if ok, retryAfter := resendRateLimiter.allow(username); !ok { w.Header().Set("Retry-After", strconv.Itoa(retryAfter)) respondResendError(w, http.StatusTooManyRequests, "please wait before requesting another email", retryAfter) return } if err := a.triggerPasswordReset(r, username); err != nil { log.Printf("resend-reset: failed for %s: %v", username, err) respondResendError(w, http.StatusInternalServerError, "failed to send email", 0) return } resendRateLimiter.record(username) log.Printf("resend-reset: password reset email sent for %s", username) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "ok": true, "message": "Password setup email sent. Check your inbox.", }) } func respondResendError(w http.ResponseWriter, code int, msg string, retryAfter int) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) body := map[string]any{"ok": false, "error": msg} if retryAfter > 0 { body["retry_after_seconds"] = retryAfter } json.NewEncoder(w).Encode(body) } func clientIP(r *http.Request) string { if s := r.Header.Get("X-Forwarded-For"); s != "" { if idx := strings.Index(s, ","); idx > 0 { return strings.TrimSpace(s[:idx]) } return strings.TrimSpace(s) } if s := r.Header.Get("X-Real-IP"); s != "" { return s } if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { return host } return r.RemoteAddr } func (a *App) triggerPasswordReset(r *http.Request, username string) error { url := a.cfg.AutheliaInternalURL + "/api/reset-password/identity/start" body, _ := json.Marshal(map[string]string{"username": username}) log.Printf("triggerPasswordReset: POST %s for user %q", url, username) req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) if err != nil { return fmt.Errorf("authelia reset build request: %w", err) } externalHost := strings.TrimPrefix(strings.TrimPrefix(a.cfg.AutheliaURL, "https://"), "http://") proto := "http" if strings.HasPrefix(a.cfg.AutheliaURL, "https://") { proto = "https" } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Forwarded-Host", externalHost) req.Header.Set("X-Forwarded-Proto", proto) req.Header.Set("X-Forwarded-For", clientIP(r)) resp, err := http.DefaultClient.Do(req) if err != nil { return fmt.Errorf("authelia reset request: %w", err) } defer resp.Body.Close() respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) log.Printf("triggerPasswordReset: status=%d body=%s", resp.StatusCode, string(respBody)) if resp.StatusCode != http.StatusOK { return fmt.Errorf("authelia reset returned %d: %s", resp.StatusCode, string(respBody)) } var result struct { Status string `json:"status"` } if json.Unmarshal(respBody, &result) == nil && result.Status == "KO" { return fmt.Errorf("authelia reset rejected: %s", string(respBody)) } return nil }