package ai import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" "github.com/ultisuite/ulti-backend/internal/auth" "github.com/ultisuite/ulti-backend/internal/config" "github.com/ultisuite/ulti-backend/internal/permission" ) const defaultOpenWebUIProfileImage = "/user.png" // SyncOpenWebUIProfile pushes profile_image_url to OpenWebUI via trusted-header auth. func SyncOpenWebUIProfile(ctx context.Context, cfg *config.Config, claims *auth.Claims, profileImageURL string) error { if cfg == nil || !cfg.AIAssistantEnabled || claims == nil { return nil } email := strings.TrimSpace(claims.Email) if email == "" { return nil } baseURL := strings.TrimRight(strings.TrimSpace(cfg.OpenWebUIInternalURL), "/") if baseURL == "" { baseURL = "http://openwebui:8080" } profileImageURL = strings.TrimSpace(profileImageURL) if profileImageURL == "" { profileImageURL = defaultOpenWebUIProfileImage } body, err := json.Marshal(map[string]string{ "profile_image_url": profileImageURL, }) if err != nil { return err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/api/v1/auths/update/profile", bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Ulti-User-Email", email) name := strings.TrimSpace(claims.Name) if name == "" { name = email } req.Header.Set("X-Ulti-User-Name", name) req.Header.Set("X-Ulti-User-Role", openWebUIRoleFromClaims(claims)) client := &http.Client{Timeout: 15 * time.Second} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 200 && resp.StatusCode < 300 { return nil } raw, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) return fmt.Errorf("openwebui profile sync: %d %s", resp.StatusCode, strings.TrimSpace(string(raw))) } func openWebUIRoleFromClaims(claims *auth.Claims) string { if claims != nil && permission.HasRole(claims.Groups, permission.RoleAdmin) { return "admin" } return "user" }