This commit is contained in:
Tomas Dvorak
2025-10-19 17:16:57 +02:00
parent e9a63073e5
commit 77213f4e83
76 changed files with 9728 additions and 935 deletions
+113 -5
View File
@@ -194,17 +194,33 @@ type UmamiAuthResponse struct {
}
type UmamiCreateWebsiteRequest struct {
Name string `json:"name"`
Domain string `json:"domain"`
Name string `json:"name"`
Domain string `json:"domain"`
TeamId *string `json:"teamId,omitempty"` // Optional: team ID for Umami v2
}
type UmamiWebsiteResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Domain string `json:"domain"`
TeamId *string `json:"teamId"`
CreatedAt time.Time `json:"createdAt"`
}
// UmamiTeam represents a team in Umami
type UmamiTeam struct {
ID string `json:"id"`
Name string `json:"name"`
}
// umamiTeamsResponse matches the shape of GET /api/users/:userId/teams
type umamiTeamsResponse struct {
Data []UmamiTeam `json:"data"`
Count int `json:"count"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
// NewUmamiService creates a new Umami service instance
func NewUmamiService() *UmamiService {
return &UmamiService{
@@ -307,15 +323,101 @@ func (u *UmamiService) authenticate() error {
return nil
}
// GetUserTeams retrieves the authenticated user's teams
func (u *UmamiService) GetUserTeams(userID string) ([]UmamiTeam, error) {
if err := u.authenticate(); err != nil {
return nil, err
}
url := fmt.Sprintf("%s/api/users/%s/teams?page=1&pageSize=10", u.baseURL, userID)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create teams request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+u.token)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send teams request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("get teams failed with status %d: %s", resp.StatusCode, string(bodyBytes))
}
var teamsResp umamiTeamsResponse
if err := json.NewDecoder(resp.Body).Decode(&teamsResp); err != nil {
return nil, fmt.Errorf("failed to decode teams response: %w", err)
}
return teamsResp.Data, nil
}
// GetCurrentUser retrieves the current authenticated user info from Umami
func (u *UmamiService) GetCurrentUser() (map[string]interface{}, error) {
if err := u.authenticate(); err != nil {
return nil, err
}
req, err := http.NewRequest("POST", u.baseURL+"/api/auth/verify", nil)
if err != nil {
return nil, fmt.Errorf("failed to create verify request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+u.token)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send verify request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("verify failed with status %d: %s", resp.StatusCode, string(bodyBytes))
}
var user map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, fmt.Errorf("failed to decode user response: %w", err)
}
return user, nil
}
// CreateWebsite creates a new website in Umami and returns the website ID
func (u *UmamiService) CreateWebsite(name, domain string) (string, error) {
if err := u.authenticate(); err != nil {
return "", err
}
logger.Info("Creating Umami website: name='%s', domain='%s'", name, domain)
// Try to get user info and teams for Umami v2 compatibility
var teamID *string
user, err := u.GetCurrentUser()
if err != nil {
logger.Warn("Failed to get current user info (continuing without team): %v", err)
} else {
if userID, ok := user["id"].(string); ok && userID != "" {
teams, err := u.GetUserTeams(userID)
if err != nil {
logger.Warn("Failed to fetch user teams (continuing without team): %v", err)
} else if len(teams) > 0 {
// Use the first available team
teamID = &teams[0].ID
logger.Info("Using team ID: %s (team name: %s)", teams[0].ID, teams[0].Name)
}
}
}
createReq := UmamiCreateWebsiteRequest{
Name: name,
Domain: domain,
TeamId: teamID,
}
body, err := json.Marshal(createReq)
@@ -323,6 +425,8 @@ func (u *UmamiService) CreateWebsite(name, domain string) (string, error) {
return "", fmt.Errorf("failed to marshal create website request: %w", err)
}
logger.Info("Sending website creation request to Umami API: %s/api/websites", u.baseURL)
req, err := http.NewRequest("POST", u.baseURL+"/api/websites", bytes.NewBuffer(body))
if err != nil {
return "", fmt.Errorf("failed to create website request: %w", err)
@@ -338,17 +442,21 @@ func (u *UmamiService) CreateWebsite(name, domain string) (string, error) {
}
defer resp.Body.Close()
// Read response body for detailed error logging
bodyBytes, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
bodyBytes, _ := io.ReadAll(resp.Body)
logger.Error("Umami website creation failed with status %d: %s", resp.StatusCode, string(bodyBytes))
return "", fmt.Errorf("create website failed with status %d: %s", resp.StatusCode, string(bodyBytes))
}
var websiteResp UmamiWebsiteResponse
if err := json.NewDecoder(resp.Body).Decode(&websiteResp); err != nil {
if err := json.Unmarshal(bodyBytes, &websiteResp); err != nil {
logger.Error("Failed to decode website response: %v, body: %s", err, string(bodyBytes))
return "", fmt.Errorf("failed to decode website response: %w", err)
}
logger.Info("Successfully created Umami website: %s (ID: %s)", name, websiteResp.ID)
logger.Info("Successfully created Umami website: %s (ID: %s, Domain: %s)", name, websiteResp.ID, websiteResp.Domain)
return websiteResp.ID, nil
}