mirror of
https://github.com/sstent/go-garth.git
synced 2025-12-06 08:01:42 +00:00
sync
This commit is contained in:
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Ignore environment files
|
||||
.env
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Build artifacts
|
||||
bin/
|
||||
dist/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
80
examples/activities/activities_example.go
Normal file
80
examples/activities/activities_example.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/sstent/go-garth"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load environment variables from .env file
|
||||
if err := godotenv.Load("../../../.env"); err != nil {
|
||||
log.Println("Note: Using system environment variables (no .env file found)")
|
||||
}
|
||||
|
||||
// Get credentials from environment
|
||||
username := os.Getenv("GARMIN_USERNAME")
|
||||
password := os.Getenv("GARMIN_PASSWORD")
|
||||
if username == "" || password == "" {
|
||||
log.Fatal("GARMIN_USERNAME or GARMIN_PASSWORD not set in environment")
|
||||
}
|
||||
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create token storage and authenticator
|
||||
storage := garth.NewMemoryStorage()
|
||||
auth := garth.NewAuthenticator(garth.ClientOptions{
|
||||
Storage: storage,
|
||||
TokenURL: "https://connectapi.garmin.com/oauth-service/oauth/token",
|
||||
Timeout: 30 * time.Second,
|
||||
})
|
||||
|
||||
// Authenticate
|
||||
token, err := auth.Login(ctx, username, password, "")
|
||||
if err != nil {
|
||||
log.Fatalf("Authentication failed: %v", err)
|
||||
}
|
||||
log.Printf("Authenticated successfully! Token expires at: %s", token.Expiry.Format(time.RFC3339))
|
||||
|
||||
// Create HTTP client with authentication transport
|
||||
httpClient := &http.Client{
|
||||
Transport: garth.NewAuthTransport(auth.(*garth.GarthAuthenticator), storage, nil),
|
||||
}
|
||||
|
||||
// Create API client
|
||||
apiClient := garth.NewAPIClient("https://connectapi.garmin.com", httpClient)
|
||||
|
||||
// Create activity service
|
||||
activityService := garth.NewActivityService(apiClient)
|
||||
|
||||
// List last 20 activities
|
||||
activities, err := activityService.List(ctx, garth.ActivityListOptions{
|
||||
Limit: 20,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get activities: %v", err)
|
||||
}
|
||||
|
||||
// Print activities
|
||||
fmt.Println("\nLast 20 Activities:")
|
||||
fmt.Println("=======================================")
|
||||
for i, activity := range activities {
|
||||
fmt.Printf("%d. %s [%s] - %s\n", i+1,
|
||||
activity.Name,
|
||||
activity.Type,
|
||||
activity.StartTime.Format("2006-01-02 15:04"))
|
||||
fmt.Printf(" Distance: %.2f km, Duration: %.0f min\n\n",
|
||||
activity.Distance/1000,
|
||||
activity.Duration/60)
|
||||
}
|
||||
|
||||
fmt.Println("Example completed successfully!")
|
||||
}
|
||||
8
examples/activities/go.mod
Normal file
8
examples/activities/go.mod
Normal file
@@ -0,0 +1,8 @@
|
||||
module github.com/sstent/go-garth/examples/activities
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/sstent/go-garth v0.1.0
|
||||
)
|
||||
80
examples/activities_example.go
Normal file
80
examples/activities_example.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/sstent/go-garth"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load environment variables from .env file
|
||||
if err := godotenv.Load("../../.env"); err != nil {
|
||||
log.Println("Note: Using system environment variables (no .env file found)")
|
||||
}
|
||||
|
||||
// Get credentials from environment
|
||||
username := os.Getenv("GARMIN_USERNAME")
|
||||
password := os.Getenv("GARMIN_PASSWORD")
|
||||
if username == "" || password == "" {
|
||||
log.Fatal("GARMIN_USERNAME or GARMIN_PASSWORD not set in environment")
|
||||
}
|
||||
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create token storage and authenticator
|
||||
storage := garth.NewMemoryStorage()
|
||||
auth := garth.NewAuthenticator(garth.ClientOptions{
|
||||
Storage: storage,
|
||||
TokenURL: "https://connectapi.garmin.com/oauth-service/oauth/token",
|
||||
Timeout: 30 * time.Second,
|
||||
})
|
||||
|
||||
// Authenticate
|
||||
token, err := auth.Login(ctx, username, password, "")
|
||||
if err != nil {
|
||||
log.Fatalf("Authentication failed: %v", err)
|
||||
}
|
||||
log.Printf("Authenticated successfully! Token expires at: %s", token.Expiry.Format(time.RFC3339))
|
||||
|
||||
// Create HTTP client with authentication transport
|
||||
httpClient := &http.Client{
|
||||
Transport: garth.NewAuthTransport(auth.(*garth.GarthAuthenticator), storage, nil),
|
||||
}
|
||||
|
||||
// Create API client
|
||||
apiClient := garth.NewAPIClient("https://connectapi.garmin.com", httpClient)
|
||||
|
||||
// Create activity service
|
||||
activityService := garth.NewActivityService(apiClient)
|
||||
|
||||
// List last 20 activities
|
||||
activities, err := activityService.List(ctx, garth.ActivityListOptions{
|
||||
Limit: 20,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get activities: %v", err)
|
||||
}
|
||||
|
||||
// Print activities
|
||||
fmt.Println("\nLast 20 Activities:")
|
||||
fmt.Println("=======================================")
|
||||
for i, activity := range activities {
|
||||
fmt.Printf("%d. %s [%s] - %s\n", i+1,
|
||||
activity.Name,
|
||||
activity.Type,
|
||||
activity.StartTime.Format("2006-01-02 15:04"))
|
||||
fmt.Printf(" Distance: %.2f km, Duration: %.0f min\n\n",
|
||||
activity.Distance/1000,
|
||||
activity.Duration/60)
|
||||
}
|
||||
|
||||
fmt.Println("Example completed successfully!")
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create a new client
|
||||
client := garth.New()
|
||||
// Create a new client (placeholder - actual client creation depends on package structure)
|
||||
// client := garth.NewClient(nil)
|
||||
|
||||
// For demonstration, we'll use a mock server or skip authentication
|
||||
// In real usage, you would authenticate first:
|
||||
@@ -42,12 +42,31 @@ func main() {
|
||||
// List workouts with options
|
||||
opts := garth.WorkoutListOptions{
|
||||
Limit: 10,
|
||||
Offset: 0,
|
||||
StartDate: time.Now().AddDate(0, -1, 0), // Last month
|
||||
EndDate: time.Now(),
|
||||
SortBy: "createdDate",
|
||||
SortOrder: "desc",
|
||||
}
|
||||
|
||||
fmt.Printf("Workout list options: %+v\n", opts)
|
||||
|
||||
// List workouts with pagination
|
||||
paginatedOpts := garth.WorkoutListOptions{
|
||||
Limit: 5,
|
||||
Offset: 10,
|
||||
SortBy: "name",
|
||||
}
|
||||
fmt.Printf("Paginated workout options: %+v\n", paginatedOpts)
|
||||
|
||||
// List workouts with type and status filters
|
||||
filteredOpts := garth.WorkoutListOptions{
|
||||
Type: "running",
|
||||
Status: "active",
|
||||
Limit: 20,
|
||||
}
|
||||
fmt.Printf("Filtered workout options: %+v\n", filteredOpts)
|
||||
|
||||
// Get workout details
|
||||
workoutID := "12345"
|
||||
fmt.Printf("Would fetch workout details for ID: %s\n", workoutID)
|
||||
|
||||
431
go_garth_todo.md
431
go_garth_todo.md
@@ -1,431 +0,0 @@
|
||||
# Go-Garth Implementation TODO List
|
||||
|
||||
## 🎯 Project Overview
|
||||
Complete the Go implementation of the Garth library to match the functionality of the original Python version for Garmin Connect authentication and API access.
|
||||
|
||||
## 📋 Phase 1: Complete Authentication Foundation (Priority: HIGH)
|
||||
|
||||
### 1.1 Fix Existing Authentication Issues
|
||||
|
||||
**Task**: Complete MFA Implementation in `auth.go`
|
||||
- **File**: `auth.go` - `handleMFA` method
|
||||
- **Requirements**:
|
||||
- Parse MFA challenge from response body
|
||||
- Submit MFA token via POST request
|
||||
- Handle MFA verification response
|
||||
- Extract and return authentication ticket
|
||||
- **Go Style Notes**:
|
||||
- Use structured error handling: `fmt.Errorf("mfa verification failed: %w", err)`
|
||||
- Validate MFA token format before sending
|
||||
- Use context for request timeouts
|
||||
- **Testing**: Create test cases for MFA flow with mock responses
|
||||
|
||||
**Task**: Implement Token Refresh Logic
|
||||
- **File**: `auth.go` - `RefreshToken` method
|
||||
- **Requirements**:
|
||||
- Implement OAuth2 refresh token flow
|
||||
- Handle refresh token expiration
|
||||
- Update stored token after successful refresh
|
||||
- Return new token with updated expiry
|
||||
- **Go Idioms**:
|
||||
```go
|
||||
// Use pointer receivers for methods that modify state
|
||||
func (a *GarthAuthenticator) RefreshToken(ctx context.Context, refreshToken string) (*Token, error) {
|
||||
// Validate input
|
||||
if refreshToken == "" {
|
||||
return nil, errors.New("refresh token cannot be empty")
|
||||
}
|
||||
// Implementation here...
|
||||
}
|
||||
```
|
||||
|
||||
**Task**: Enhance Error Handling
|
||||
- **Files**: `auth.go`, `types.go`
|
||||
- **Requirements**:
|
||||
- Create custom error types for different failure modes
|
||||
- Add HTTP status code context to errors
|
||||
- Parse Garmin-specific error responses
|
||||
- **Implementation**:
|
||||
```go
|
||||
// types.go
|
||||
type AuthError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func (e *AuthError) Error() string {
|
||||
return fmt.Sprintf("garmin auth error %d: %s", e.Code, e.Message)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 Improve HTTP Client Architecture
|
||||
|
||||
**Task**: Create Authenticated HTTP Client Middleware
|
||||
- **New File**: `client.go`
|
||||
- **Requirements**:
|
||||
- Implement `http.RoundTripper` interface
|
||||
- Automatically add authentication headers
|
||||
- Handle token refresh on 401 responses
|
||||
- Add request/response logging (optional)
|
||||
- **Go Pattern**:
|
||||
```go
|
||||
type AuthTransport struct {
|
||||
base http.RoundTripper
|
||||
auth *GarthAuthenticator
|
||||
storage TokenStorage
|
||||
}
|
||||
|
||||
func (t *AuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// Clone request, add auth headers, handle refresh
|
||||
}
|
||||
```
|
||||
|
||||
**Task**: Add Request Retry Logic
|
||||
- **File**: `client.go`
|
||||
- **Requirements**:
|
||||
- Implement exponential backoff
|
||||
- Retry on specific HTTP status codes (500, 502, 503)
|
||||
- Maximum retry attempts configuration
|
||||
- Context-aware cancellation
|
||||
- **Go Style**: Use `time.After` and `select` for backoff timing
|
||||
|
||||
### 1.3 Expand Storage Options
|
||||
|
||||
**Task**: Add Memory-based Token Storage
|
||||
- **New File**: `memorystorage.go`
|
||||
- **Requirements**:
|
||||
- Implement `TokenStorage` interface
|
||||
- Thread-safe operations using `sync.RWMutex`
|
||||
- Optional token encryption in memory
|
||||
- **Go Concurrency**:
|
||||
```go
|
||||
type MemoryStorage struct {
|
||||
mu sync.RWMutex
|
||||
token *Token
|
||||
}
|
||||
```
|
||||
|
||||
**Task**: Environment Variable Configuration
|
||||
- **File**: `garth.go`
|
||||
- **Requirements**:
|
||||
- Load configuration from environment variables
|
||||
- Provide reasonable defaults
|
||||
- Support custom configuration via struct
|
||||
- **Go Standard**: Use `os.Getenv()` and provide defaults
|
||||
|
||||
## 📋 Phase 2: Garmin Connect API Client (Priority: HIGH)
|
||||
|
||||
### 2.1 Core API Client Structure
|
||||
|
||||
**Task**: Create Base API Client
|
||||
- **New File**: `connect.go`
|
||||
- **Requirements**:
|
||||
- Embed authenticated HTTP client
|
||||
- Base URL configuration for different Garmin services
|
||||
- Common request/response handling
|
||||
- Rate limiting support
|
||||
- **Structure**:
|
||||
```go
|
||||
type ConnectClient struct {
|
||||
client *http.Client
|
||||
baseURL string
|
||||
userAgent string
|
||||
auth Authenticator
|
||||
}
|
||||
|
||||
func NewConnectClient(auth Authenticator, opts ConnectOptions) *ConnectClient
|
||||
```
|
||||
|
||||
**Task**: Implement Common HTTP Helpers
|
||||
- **File**: `connect.go`
|
||||
- **Requirements**:
|
||||
- Generic GET, POST, PUT, DELETE methods
|
||||
- JSON request/response marshaling
|
||||
- Query parameter handling
|
||||
- Error response parsing
|
||||
- **Go Generics** (Go 1.18+):
|
||||
```go
|
||||
func (c *ConnectClient) Get[T any](ctx context.Context, endpoint string, result *T) error
|
||||
```
|
||||
|
||||
### 2.2 User Profile and Account APIs
|
||||
|
||||
**Task**: User Profile Management
|
||||
- **New File**: `profile.go`
|
||||
- **Requirements**:
|
||||
- Get user profile information
|
||||
- Update profile settings
|
||||
- Account preferences
|
||||
- **Types**:
|
||||
```go
|
||||
type UserProfile struct {
|
||||
UserID int64 `json:"userId"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Email string `json:"email"`
|
||||
// Add other profile fields
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Activity and Workout APIs
|
||||
|
||||
**Task**: Activity Data Retrieval
|
||||
- **New File**: `activities.go`
|
||||
- **Requirements**:
|
||||
- List activities with pagination
|
||||
- Get detailed activity data
|
||||
- Activity search and filtering
|
||||
- Export activity data (GPX, TCX, etc.)
|
||||
- **Pagination Pattern**:
|
||||
```go
|
||||
type ActivityListOptions struct {
|
||||
Start int `json:"start"`
|
||||
Limit int `json:"limit"`
|
||||
// Add filter options
|
||||
}
|
||||
```
|
||||
|
||||
**Task**: Workout Management
|
||||
- **New File**: `workouts.go`
|
||||
- **Requirements**:
|
||||
- Create, read, update, delete workouts
|
||||
- Workout scheduling
|
||||
- Workout templates
|
||||
- **CRUD Pattern**: Follow consistent naming (Create, Get, Update, Delete methods)
|
||||
|
||||
## 📋 Phase 3: Advanced Features (Priority: MEDIUM)
|
||||
|
||||
### 3.1 Device and Sync Management
|
||||
|
||||
**Task**: Device Information APIs
|
||||
- **New File**: `devices.go`
|
||||
- **Requirements**:
|
||||
- List connected devices
|
||||
- Device settings and preferences
|
||||
- Sync status and history
|
||||
- **Go Struct Tags**: Use proper JSON tags for API marshaling
|
||||
|
||||
### 3.2 Health and Metrics APIs
|
||||
|
||||
**Task**: Health Data Access
|
||||
- **New File**: `health.go`
|
||||
- **Requirements**:
|
||||
- Daily summaries (steps, calories, etc.)
|
||||
- Sleep data
|
||||
- Heart rate data
|
||||
- Weight and body composition
|
||||
- **Time Handling**: Use `time.Time` for all timestamps, handle timezone conversions
|
||||
|
||||
### 3.3 Social and Challenges
|
||||
|
||||
**Task**: Social Features
|
||||
- **New File**: `social.go`
|
||||
- **Requirements**:
|
||||
- Friends and connections
|
||||
- Activity sharing
|
||||
- Challenges and competitions
|
||||
- **Privacy Considerations**: Add warnings about sharing personal data
|
||||
|
||||
## 📋 Phase 4: Developer Experience (Priority: MEDIUM)
|
||||
|
||||
### 4.1 Testing Infrastructure
|
||||
|
||||
**Task**: Unit Tests for Authentication
|
||||
- **New File**: `auth_test.go`
|
||||
- **Requirements**:
|
||||
- Mock HTTP server for testing
|
||||
- Test all authentication flows
|
||||
- Error condition coverage
|
||||
- Use `httptest.Server` for mocking
|
||||
- **Go Testing Pattern**:
|
||||
```go
|
||||
func TestGarthAuthenticator_Login(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
// test cases
|
||||
}{
|
||||
// table-driven tests
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Task**: Integration Tests
|
||||
- **New File**: `integration_test.go`
|
||||
- **Requirements**:
|
||||
- End-to-end API tests (optional, requires credentials)
|
||||
- Build tag for integration tests: `//go:build integration`
|
||||
- Environment variable configuration for test credentials
|
||||
|
||||
### 4.2 Documentation and Examples
|
||||
|
||||
**Task**: Package Documentation
|
||||
- **All Files**: Add comprehensive GoDoc comments
|
||||
- **Requirements**:
|
||||
- Package-level documentation in `garth.go`
|
||||
- Example usage in doc comments
|
||||
- Follow Go documentation conventions
|
||||
- **GoDoc Style**:
|
||||
```go
|
||||
// Package garth provides authentication and API access for Garmin Connect services.
|
||||
//
|
||||
// Basic usage:
|
||||
//
|
||||
// auth := garth.NewAuthenticator(garth.ClientOptions{...})
|
||||
// token, err := auth.Login(ctx, username, password, "")
|
||||
//
|
||||
package garth
|
||||
```
|
||||
|
||||
**Task**: Create Usage Examples
|
||||
- **New Directory**: `examples/`
|
||||
- **Requirements**:
|
||||
- Basic authentication example
|
||||
- Activity data retrieval example
|
||||
- Complete CLI tool example
|
||||
- README with setup instructions
|
||||
|
||||
### 4.3 Configuration and Logging
|
||||
|
||||
**Task**: Structured Logging Support
|
||||
- **New File**: `logging.go`
|
||||
- **Requirements**:
|
||||
- Optional logger interface
|
||||
- Debug logging for HTTP requests/responses
|
||||
- Configurable log levels
|
||||
- **Go Interface**:
|
||||
```go
|
||||
type Logger interface {
|
||||
Debug(msg string, fields ...interface{})
|
||||
Info(msg string, fields ...interface{})
|
||||
Error(msg string, fields ...interface{})
|
||||
}
|
||||
```
|
||||
|
||||
**Task**: Configuration Management
|
||||
- **File**: `config.go`
|
||||
- **Requirements**:
|
||||
- Centralized configuration struct
|
||||
- Environment variable loading
|
||||
- Configuration validation
|
||||
- Default values
|
||||
|
||||
## 📋 Phase 5: Production Readiness (Priority: LOW)
|
||||
|
||||
### 5.1 Performance and Reliability
|
||||
|
||||
**Task**: Add Rate Limiting
|
||||
- **File**: `client.go` or new `ratelimit.go`
|
||||
- **Requirements**:
|
||||
- Token bucket algorithm
|
||||
- Configurable rate limits
|
||||
- Per-endpoint rate limiting if needed
|
||||
- **Go Concurrency**: Use goroutines and channels for rate limiting
|
||||
|
||||
**Task**: Connection Pooling and Timeouts
|
||||
- **File**: `client.go`
|
||||
- **Requirements**:
|
||||
- Configure HTTP client with appropriate timeouts
|
||||
- Connection pooling settings
|
||||
- Keep-alive configuration
|
||||
|
||||
### 5.2 Security Enhancements
|
||||
|
||||
**Task**: Token Encryption at Rest
|
||||
- **File**: `filestorage.go`, new `encryption.go`
|
||||
- **Requirements**:
|
||||
- Optional token encryption using AES
|
||||
- Key derivation from user password or system keyring
|
||||
- Secure key storage recommendations
|
||||
|
||||
### 5.3 Monitoring and Metrics
|
||||
|
||||
**Task**: Add Metrics Collection
|
||||
- **New File**: `metrics.go`
|
||||
- **Requirements**:
|
||||
- Request/response metrics
|
||||
- Error rate tracking
|
||||
- Optional Prometheus metrics export
|
||||
- **Go Pattern**: Use interfaces for pluggable metrics backends
|
||||
|
||||
## 🎨 Go Style and Idiom Guidelines
|
||||
|
||||
### Code Organization
|
||||
- **Package Structure**: Keep related functionality in separate files
|
||||
- **Interfaces**: Define small, focused interfaces
|
||||
- **Error Handling**: Always handle errors, use `fmt.Errorf` for context
|
||||
- **Context**: Pass context.Context as first parameter to all long-running functions
|
||||
|
||||
### Naming Conventions
|
||||
- **Exported Types**: PascalCase (e.g., `GarthAuthenticator`)
|
||||
- **Unexported Types**: camelCase (e.g., `fileStorage`)
|
||||
- **Methods**: Use verb-noun pattern (e.g., `GetToken`, `SaveToken`)
|
||||
- **Constants**: Use PascalCase for exported, camelCase for unexported
|
||||
|
||||
### Error Handling Patterns
|
||||
```go
|
||||
// Wrap errors with context
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to authenticate user %s: %w", username, err)
|
||||
}
|
||||
|
||||
// Use custom error types for different error conditions
|
||||
type AuthenticationError struct {
|
||||
Code int
|
||||
Message string
|
||||
Cause error
|
||||
}
|
||||
```
|
||||
|
||||
### JSON and HTTP Patterns
|
||||
```go
|
||||
// Use struct tags for JSON marshaling
|
||||
type Activity struct {
|
||||
ID int64 `json:"activityId"`
|
||||
Name string `json:"activityName"`
|
||||
StartTime time.Time `json:"startTimeLocal"`
|
||||
}
|
||||
|
||||
// Handle HTTP responses consistently
|
||||
func (c *ConnectClient) makeRequest(ctx context.Context, method, url string, body interface{}) (*http.Response, error) {
|
||||
// Implementation with consistent error handling
|
||||
}
|
||||
```
|
||||
|
||||
### Concurrency Best Practices
|
||||
- Use `sync.RWMutex` for read-heavy workloads
|
||||
- Prefer channels for communication between goroutines
|
||||
- Always handle context cancellation
|
||||
- Use `sync.WaitGroup` for waiting on multiple goroutines
|
||||
|
||||
### Testing Patterns
|
||||
- Use table-driven tests for multiple test cases
|
||||
- Create test helpers for common setup
|
||||
- Use `httptest.Server` for HTTP testing
|
||||
- Mock external dependencies using interfaces
|
||||
|
||||
## 🚀 Implementation Priority Order
|
||||
|
||||
1. **Week 1**: Complete authentication foundation (Phase 1)
|
||||
2. **Week 2-3**: Implement core API client and activity APIs (Phase 2.1, 2.3)
|
||||
3. **Week 4**: Add user profile and device APIs (Phase 2.2, 3.1)
|
||||
4. **Week 5**: Testing and documentation (Phase 4)
|
||||
5. **Week 6+**: Advanced features and production readiness (Phase 3, 5)
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
- [Effective Go](https://golang.org/doc/effective_go.html)
|
||||
- [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
|
||||
- [Original Garth Python Library](https://github.com/matin/garth) - Reference implementation
|
||||
- [Garmin Connect API Documentation](https://connect.garmin.com/dev/) - If available
|
||||
- [OAuth 2.0 RFC](https://tools.ietf.org/html/rfc6749) - Understanding the auth flow
|
||||
|
||||
## ✅ Definition of Done
|
||||
|
||||
Each task is complete when:
|
||||
- [ ] Code follows Go best practices and style guidelines
|
||||
- [ ] All public functions have GoDoc comments
|
||||
- [ ] Unit tests achieve >80% coverage
|
||||
- [ ] Integration tests pass (where applicable)
|
||||
- [ ] No linting errors from `golangci-lint`
|
||||
- [ ] Code is reviewed by senior developer
|
||||
- [ ] Examples and documentation are updated
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
login_page_1756913256.html
Normal file
BIN
login_page_1756913256.html
Normal file
Binary file not shown.
@@ -1,21 +0,0 @@
|
||||
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json # Schema for CodeRabbit configurations
|
||||
language: "en-US"
|
||||
early_access: true
|
||||
reviews:
|
||||
request_changes_workflow: false
|
||||
high_level_summary: true
|
||||
poem: false
|
||||
review_status: true
|
||||
collapse_walkthrough: false
|
||||
auto_review:
|
||||
enabled: true
|
||||
drafts: false
|
||||
path_filters:
|
||||
- "!tests/**/cassettes/**"
|
||||
path_instructions:
|
||||
- path: "tests/**"
|
||||
instructions: |
|
||||
- test functions shouldn't have a return type hint
|
||||
- it's ok to use `assert` instead of `pytest.assume()`
|
||||
chat:
|
||||
auto_reply: true
|
||||
@@ -1,7 +0,0 @@
|
||||
FROM mcr.microsoft.com/devcontainers/anaconda:0-3
|
||||
|
||||
# Copy environment.yml (if found) to a temp location so we update the environment. Also
|
||||
# copy "noop.txt" so the COPY instruction does not fail if no environment.yml exists.
|
||||
COPY environment.yml* .devcontainer/noop.txt /tmp/conda-tmp/
|
||||
RUN if [ -f "/tmp/conda-tmp/environment.yml" ]; then umask 0002 && /opt/conda/bin/conda env update -n base -f /tmp/conda-tmp/environment.yml; fi \
|
||||
&& rm -rf /tmp/conda-tmp
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "Anaconda (Python 3)",
|
||||
"build": {
|
||||
"context": "..",
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
This file copied into the container along with environment.yml* from the parent
|
||||
folder. This file is included to prevents the Dockerfile COPY instruction from
|
||||
failing if no environment.yml is found.
|
||||
@@ -1 +0,0 @@
|
||||
ref: refs/heads/main
|
||||
@@ -1,12 +0,0 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
[remote "origin"]
|
||||
url = https://github.com/matin/garth.git
|
||||
fetch = +refs/heads/*:refs/remotes/origin/*
|
||||
[branch "main"]
|
||||
remote = origin
|
||||
merge = refs/heads/main
|
||||
vscode-merge-base = origin/main
|
||||
@@ -1 +0,0 @@
|
||||
Unnamed repository; edit this file 'description' to name the repository.
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message taken by
|
||||
# applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit. The hook is
|
||||
# allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "applypatch-msg".
|
||||
|
||||
. git-sh-setup
|
||||
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
|
||||
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
|
||||
:
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message.
|
||||
# Called by "git commit" with one argument, the name of the file
|
||||
# that has the commit message. The hook should exit with non-zero
|
||||
# status after issuing an appropriate message if it wants to stop the
|
||||
# commit. The hook is allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "commit-msg".
|
||||
|
||||
# Uncomment the below to add a Signed-off-by line to the message.
|
||||
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
|
||||
# hook is more suited to it.
|
||||
#
|
||||
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
|
||||
|
||||
# This example catches duplicate Signed-off-by lines.
|
||||
|
||||
test "" = "$(grep '^Signed-off-by: ' "$1" |
|
||||
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
|
||||
echo >&2 Duplicate Signed-off-by lines.
|
||||
exit 1
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use IPC::Open2;
|
||||
|
||||
# An example hook script to integrate Watchman
|
||||
# (https://facebook.github.io/watchman/) with git to speed up detecting
|
||||
# new and modified files.
|
||||
#
|
||||
# The hook is passed a version (currently 2) and last update token
|
||||
# formatted as a string and outputs to stdout a new update token and
|
||||
# all files that have been modified since the update token. Paths must
|
||||
# be relative to the root of the working tree and separated by a single NUL.
|
||||
#
|
||||
# To enable this hook, rename this file to "query-watchman" and set
|
||||
# 'git config core.fsmonitor .git/hooks/query-watchman'
|
||||
#
|
||||
my ($version, $last_update_token) = @ARGV;
|
||||
|
||||
# Uncomment for debugging
|
||||
# print STDERR "$0 $version $last_update_token\n";
|
||||
|
||||
# Check the hook interface version
|
||||
if ($version ne 2) {
|
||||
die "Unsupported query-fsmonitor hook version '$version'.\n" .
|
||||
"Falling back to scanning...\n";
|
||||
}
|
||||
|
||||
my $git_work_tree = get_working_dir();
|
||||
|
||||
my $retry = 1;
|
||||
|
||||
my $json_pkg;
|
||||
eval {
|
||||
require JSON::XS;
|
||||
$json_pkg = "JSON::XS";
|
||||
1;
|
||||
} or do {
|
||||
require JSON::PP;
|
||||
$json_pkg = "JSON::PP";
|
||||
};
|
||||
|
||||
launch_watchman();
|
||||
|
||||
sub launch_watchman {
|
||||
my $o = watchman_query();
|
||||
if (is_work_tree_watched($o)) {
|
||||
output_result($o->{clock}, @{$o->{files}});
|
||||
}
|
||||
}
|
||||
|
||||
sub output_result {
|
||||
my ($clockid, @files) = @_;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# binmode $fh, ":utf8";
|
||||
# print $fh "$clockid\n@files\n";
|
||||
# close $fh;
|
||||
|
||||
binmode STDOUT, ":utf8";
|
||||
print $clockid;
|
||||
print "\0";
|
||||
local $, = "\0";
|
||||
print @files;
|
||||
}
|
||||
|
||||
sub watchman_clock {
|
||||
my $response = qx/watchman clock "$git_work_tree"/;
|
||||
die "Failed to get clock id on '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub watchman_query {
|
||||
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
|
||||
or die "open2() failed: $!\n" .
|
||||
"Falling back to scanning...\n";
|
||||
|
||||
# In the query expression below we're asking for names of files that
|
||||
# changed since $last_update_token but not from the .git folder.
|
||||
#
|
||||
# To accomplish this, we're using the "since" generator to use the
|
||||
# recency index to select candidate nodes and "fields" to limit the
|
||||
# output to file names only. Then we're using the "expression" term to
|
||||
# further constrain the results.
|
||||
my $last_update_line = "";
|
||||
if (substr($last_update_token, 0, 1) eq "c") {
|
||||
$last_update_token = "\"$last_update_token\"";
|
||||
$last_update_line = qq[\n"since": $last_update_token,];
|
||||
}
|
||||
my $query = <<" END";
|
||||
["query", "$git_work_tree", {$last_update_line
|
||||
"fields": ["name"],
|
||||
"expression": ["not", ["dirname", ".git"]]
|
||||
}]
|
||||
END
|
||||
|
||||
# Uncomment for debugging the watchman query
|
||||
# open (my $fh, ">", ".git/watchman-query.json");
|
||||
# print $fh $query;
|
||||
# close $fh;
|
||||
|
||||
print CHLD_IN $query;
|
||||
close CHLD_IN;
|
||||
my $response = do {local $/; <CHLD_OUT>};
|
||||
|
||||
# Uncomment for debugging the watch response
|
||||
# open ($fh, ">", ".git/watchman-response.json");
|
||||
# print $fh $response;
|
||||
# close $fh;
|
||||
|
||||
die "Watchman: command returned no output.\n" .
|
||||
"Falling back to scanning...\n" if $response eq "";
|
||||
die "Watchman: command returned invalid output: $response\n" .
|
||||
"Falling back to scanning...\n" unless $response =~ /^\{/;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub is_work_tree_watched {
|
||||
my ($output) = @_;
|
||||
my $error = $output->{error};
|
||||
if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
|
||||
$retry--;
|
||||
my $response = qx/watchman watch "$git_work_tree"/;
|
||||
die "Failed to make watchman watch '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
$output = $json_pkg->new->utf8->decode($response);
|
||||
$error = $output->{error};
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# close $fh;
|
||||
|
||||
# Watchman will always return all files on the first query so
|
||||
# return the fast "everything is dirty" flag to git and do the
|
||||
# Watchman query just to get it over with now so we won't pay
|
||||
# the cost in git to look up each individual file.
|
||||
my $o = watchman_clock();
|
||||
$error = $output->{error};
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
output_result($o->{clock}, ("/"));
|
||||
$last_update_token = $o->{clock};
|
||||
|
||||
eval { launch_watchman() };
|
||||
return 0;
|
||||
}
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub get_working_dir {
|
||||
my $working_dir;
|
||||
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
|
||||
$working_dir = Win32::GetCwd();
|
||||
$working_dir =~ tr/\\/\//;
|
||||
} else {
|
||||
require Cwd;
|
||||
$working_dir = Cwd::cwd();
|
||||
}
|
||||
|
||||
return $working_dir;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare a packed repository for use over
|
||||
# dumb transports.
|
||||
#
|
||||
# To enable this hook, rename this file to "post-update".
|
||||
|
||||
exec git update-server-info
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed
|
||||
# by applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-applypatch".
|
||||
|
||||
. git-sh-setup
|
||||
precommit="$(git rev-parse --git-path hooks/pre-commit)"
|
||||
test -x "$precommit" && exec "$precommit" ${1+"$@"}
|
||||
:
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git commit" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message if
|
||||
# it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-commit".
|
||||
|
||||
if git rev-parse --verify HEAD >/dev/null 2>&1
|
||||
then
|
||||
against=HEAD
|
||||
else
|
||||
# Initial commit: diff against an empty tree object
|
||||
against=$(git hash-object -t tree /dev/null)
|
||||
fi
|
||||
|
||||
# If you want to allow non-ASCII filenames set this variable to true.
|
||||
allownonascii=$(git config --type=bool hooks.allownonascii)
|
||||
|
||||
# Redirect output to stderr.
|
||||
exec 1>&2
|
||||
|
||||
# Cross platform projects tend to avoid non-ASCII filenames; prevent
|
||||
# them from being added to the repository. We exploit the fact that the
|
||||
# printable range starts at the space character and ends with tilde.
|
||||
if [ "$allownonascii" != "true" ] &&
|
||||
# Note that the use of brackets around a tr range is ok here, (it's
|
||||
# even required, for portability to Solaris 10's /usr/bin/tr), since
|
||||
# the square bracket bytes happen to fall in the designated range.
|
||||
test $(git diff-index --cached --name-only --diff-filter=A -z $against |
|
||||
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
|
||||
then
|
||||
cat <<\EOF
|
||||
Error: Attempt to add a non-ASCII file name.
|
||||
|
||||
This can cause problems if you want to work with people on other platforms.
|
||||
|
||||
To be portable it is advisable to rename the file.
|
||||
|
||||
If you know what you are doing you can disable this check using:
|
||||
|
||||
git config hooks.allownonascii true
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If there are whitespace errors, print the offending file names and fail.
|
||||
exec git diff-index --check --cached $against --
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git merge" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message to
|
||||
# stderr if it wants to stop the merge commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-merge-commit".
|
||||
|
||||
. git-sh-setup
|
||||
test -x "$GIT_DIR/hooks/pre-commit" &&
|
||||
exec "$GIT_DIR/hooks/pre-commit"
|
||||
:
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# An example hook script to verify what is about to be pushed. Called by "git
|
||||
# push" after it has checked the remote status, but before anything has been
|
||||
# pushed. If this script exits with a non-zero status nothing will be pushed.
|
||||
#
|
||||
# This hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- Name of the remote to which the push is being done
|
||||
# $2 -- URL to which the push is being done
|
||||
#
|
||||
# If pushing without using a named remote those arguments will be equal.
|
||||
#
|
||||
# Information about the commits which are being pushed is supplied as lines to
|
||||
# the standard input in the form:
|
||||
#
|
||||
# <local ref> <local oid> <remote ref> <remote oid>
|
||||
#
|
||||
# This sample shows how to prevent push of commits where the log message starts
|
||||
# with "WIP" (work in progress).
|
||||
|
||||
remote="$1"
|
||||
url="$2"
|
||||
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
|
||||
while read local_ref local_oid remote_ref remote_oid
|
||||
do
|
||||
if test "$local_oid" = "$zero"
|
||||
then
|
||||
# Handle delete
|
||||
:
|
||||
else
|
||||
if test "$remote_oid" = "$zero"
|
||||
then
|
||||
# New branch, examine all commits
|
||||
range="$local_oid"
|
||||
else
|
||||
# Update to existing branch, examine new commits
|
||||
range="$remote_oid..$local_oid"
|
||||
fi
|
||||
|
||||
# Check for WIP commit
|
||||
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
|
||||
if test -n "$commit"
|
||||
then
|
||||
echo >&2 "Found WIP commit in $local_ref, not pushing"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
exit 0
|
||||
@@ -1,169 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Copyright (c) 2006, 2008 Junio C Hamano
|
||||
#
|
||||
# The "pre-rebase" hook is run just before "git rebase" starts doing
|
||||
# its job, and can prevent the command from running by exiting with
|
||||
# non-zero status.
|
||||
#
|
||||
# The hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- the upstream the series was forked from.
|
||||
# $2 -- the branch being rebased (or empty when rebasing the current branch).
|
||||
#
|
||||
# This sample shows how to prevent topic branches that are already
|
||||
# merged to 'next' branch from getting rebased, because allowing it
|
||||
# would result in rebasing already published history.
|
||||
|
||||
publish=next
|
||||
basebranch="$1"
|
||||
if test "$#" = 2
|
||||
then
|
||||
topic="refs/heads/$2"
|
||||
else
|
||||
topic=`git symbolic-ref HEAD` ||
|
||||
exit 0 ;# we do not interrupt rebasing detached HEAD
|
||||
fi
|
||||
|
||||
case "$topic" in
|
||||
refs/heads/??/*)
|
||||
;;
|
||||
*)
|
||||
exit 0 ;# we do not interrupt others.
|
||||
;;
|
||||
esac
|
||||
|
||||
# Now we are dealing with a topic branch being rebased
|
||||
# on top of master. Is it OK to rebase it?
|
||||
|
||||
# Does the topic really exist?
|
||||
git show-ref -q "$topic" || {
|
||||
echo >&2 "No such branch $topic"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Is topic fully merged to master?
|
||||
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
|
||||
if test -z "$not_in_master"
|
||||
then
|
||||
echo >&2 "$topic is fully merged to master; better remove it."
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
fi
|
||||
|
||||
# Is topic ever merged to next? If so you should not be rebasing it.
|
||||
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
|
||||
only_next_2=`git rev-list ^master ${publish} | sort`
|
||||
if test "$only_next_1" = "$only_next_2"
|
||||
then
|
||||
not_in_topic=`git rev-list "^$topic" master`
|
||||
if test -z "$not_in_topic"
|
||||
then
|
||||
echo >&2 "$topic is already up to date with master"
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
|
||||
/usr/bin/perl -e '
|
||||
my $topic = $ARGV[0];
|
||||
my $msg = "* $topic has commits already merged to public branch:\n";
|
||||
my (%not_in_next) = map {
|
||||
/^([0-9a-f]+) /;
|
||||
($1 => 1);
|
||||
} split(/\n/, $ARGV[1]);
|
||||
for my $elem (map {
|
||||
/^([0-9a-f]+) (.*)$/;
|
||||
[$1 => $2];
|
||||
} split(/\n/, $ARGV[2])) {
|
||||
if (!exists $not_in_next{$elem->[0]}) {
|
||||
if ($msg) {
|
||||
print STDERR $msg;
|
||||
undef $msg;
|
||||
}
|
||||
print STDERR " $elem->[1]\n";
|
||||
}
|
||||
}
|
||||
' "$topic" "$not_in_next" "$not_in_master"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
<<\DOC_END
|
||||
|
||||
This sample hook safeguards topic branches that have been
|
||||
published from being rewound.
|
||||
|
||||
The workflow assumed here is:
|
||||
|
||||
* Once a topic branch forks from "master", "master" is never
|
||||
merged into it again (either directly or indirectly).
|
||||
|
||||
* Once a topic branch is fully cooked and merged into "master",
|
||||
it is deleted. If you need to build on top of it to correct
|
||||
earlier mistakes, a new topic branch is created by forking at
|
||||
the tip of the "master". This is not strictly necessary, but
|
||||
it makes it easier to keep your history simple.
|
||||
|
||||
* Whenever you need to test or publish your changes to topic
|
||||
branches, merge them into "next" branch.
|
||||
|
||||
The script, being an example, hardcodes the publish branch name
|
||||
to be "next", but it is trivial to make it configurable via
|
||||
$GIT_DIR/config mechanism.
|
||||
|
||||
With this workflow, you would want to know:
|
||||
|
||||
(1) ... if a topic branch has ever been merged to "next". Young
|
||||
topic branches can have stupid mistakes you would rather
|
||||
clean up before publishing, and things that have not been
|
||||
merged into other branches can be easily rebased without
|
||||
affecting other people. But once it is published, you would
|
||||
not want to rewind it.
|
||||
|
||||
(2) ... if a topic branch has been fully merged to "master".
|
||||
Then you can delete it. More importantly, you should not
|
||||
build on top of it -- other people may already want to
|
||||
change things related to the topic as patches against your
|
||||
"master", so if you need further changes, it is better to
|
||||
fork the topic (perhaps with the same name) afresh from the
|
||||
tip of "master".
|
||||
|
||||
Let's look at this example:
|
||||
|
||||
o---o---o---o---o---o---o---o---o---o "next"
|
||||
/ / / /
|
||||
/ a---a---b A / /
|
||||
/ / / /
|
||||
/ / c---c---c---c B /
|
||||
/ / / \ /
|
||||
/ / / b---b C \ /
|
||||
/ / / / \ /
|
||||
---o---o---o---o---o---o---o---o---o---o---o "master"
|
||||
|
||||
|
||||
A, B and C are topic branches.
|
||||
|
||||
* A has one fix since it was merged up to "next".
|
||||
|
||||
* B has finished. It has been fully merged up to "master" and "next",
|
||||
and is ready to be deleted.
|
||||
|
||||
* C has not merged to "next" at all.
|
||||
|
||||
We would want to allow C to be rebased, refuse A, and encourage
|
||||
B to be deleted.
|
||||
|
||||
To compute (1):
|
||||
|
||||
git rev-list ^master ^topic next
|
||||
git rev-list ^master next
|
||||
|
||||
if these match, topic has not merged in next at all.
|
||||
|
||||
To compute (2):
|
||||
|
||||
git rev-list master..topic
|
||||
|
||||
if this is empty, it is fully merged to "master".
|
||||
|
||||
DOC_END
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to make use of push options.
|
||||
# The example simply echoes all push options that start with 'echoback='
|
||||
# and rejects all pushes when the "reject" push option is used.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-receive".
|
||||
|
||||
if test -n "$GIT_PUSH_OPTION_COUNT"
|
||||
then
|
||||
i=0
|
||||
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
|
||||
do
|
||||
eval "value=\$GIT_PUSH_OPTION_$i"
|
||||
case "$value" in
|
||||
echoback=*)
|
||||
echo "echo from the pre-receive-hook: ${value#*=}" >&2
|
||||
;;
|
||||
reject)
|
||||
exit 1
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
fi
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare the commit log message.
|
||||
# Called by "git commit" with the name of the file that has the
|
||||
# commit message, followed by the description of the commit
|
||||
# message's source. The hook's purpose is to edit the commit
|
||||
# message file. If the hook fails with a non-zero status,
|
||||
# the commit is aborted.
|
||||
#
|
||||
# To enable this hook, rename this file to "prepare-commit-msg".
|
||||
|
||||
# This hook includes three examples. The first one removes the
|
||||
# "# Please enter the commit message..." help message.
|
||||
#
|
||||
# The second includes the output of "git diff --name-status -r"
|
||||
# into the message, just before the "git status" output. It is
|
||||
# commented because it doesn't cope with --amend or with squashed
|
||||
# commits.
|
||||
#
|
||||
# The third example adds a Signed-off-by line to the message, that can
|
||||
# still be edited. This is rarely a good idea.
|
||||
|
||||
COMMIT_MSG_FILE=$1
|
||||
COMMIT_SOURCE=$2
|
||||
SHA1=$3
|
||||
|
||||
/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
|
||||
|
||||
# case "$COMMIT_SOURCE,$SHA1" in
|
||||
# ,|template,)
|
||||
# /usr/bin/perl -i.bak -pe '
|
||||
# print "\n" . `git diff --cached --name-status -r`
|
||||
# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
|
||||
# *) ;;
|
||||
# esac
|
||||
|
||||
# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
|
||||
# if test -z "$COMMIT_SOURCE"
|
||||
# then
|
||||
# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
|
||||
# fi
|
||||
@@ -1,78 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# An example hook script to update a checked-out tree on a git push.
|
||||
#
|
||||
# This hook is invoked by git-receive-pack(1) when it reacts to git
|
||||
# push and updates reference(s) in its repository, and when the push
|
||||
# tries to update the branch that is currently checked out and the
|
||||
# receive.denyCurrentBranch configuration variable is set to
|
||||
# updateInstead.
|
||||
#
|
||||
# By default, such a push is refused if the working tree and the index
|
||||
# of the remote repository has any difference from the currently
|
||||
# checked out commit; when both the working tree and the index match
|
||||
# the current commit, they are updated to match the newly pushed tip
|
||||
# of the branch. This hook is to be used to override the default
|
||||
# behaviour; however the code below reimplements the default behaviour
|
||||
# as a starting point for convenient modification.
|
||||
#
|
||||
# The hook receives the commit with which the tip of the current
|
||||
# branch is going to be updated:
|
||||
commit=$1
|
||||
|
||||
# It can exit with a non-zero status to refuse the push (when it does
|
||||
# so, it must not modify the index or the working tree).
|
||||
die () {
|
||||
echo >&2 "$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Or it can make any necessary changes to the working tree and to the
|
||||
# index to bring them to the desired state when the tip of the current
|
||||
# branch is updated to the new commit, and exit with a zero status.
|
||||
#
|
||||
# For example, the hook can simply run git read-tree -u -m HEAD "$1"
|
||||
# in order to emulate git fetch that is run in the reverse direction
|
||||
# with git push, as the two-tree form of git read-tree -u -m is
|
||||
# essentially the same as git switch or git checkout that switches
|
||||
# branches while keeping the local changes in the working tree that do
|
||||
# not interfere with the difference between the branches.
|
||||
|
||||
# The below is a more-or-less exact translation to shell of the C code
|
||||
# for the default behaviour for git's push-to-checkout hook defined in
|
||||
# the push_to_deploy() function in builtin/receive-pack.c.
|
||||
#
|
||||
# Note that the hook will be executed from the repository directory,
|
||||
# not from the working tree, so if you want to perform operations on
|
||||
# the working tree, you will have to adapt your code accordingly, e.g.
|
||||
# by adding "cd .." or using relative paths.
|
||||
|
||||
if ! git update-index -q --ignore-submodules --refresh
|
||||
then
|
||||
die "Up-to-date check failed"
|
||||
fi
|
||||
|
||||
if ! git diff-files --quiet --ignore-submodules --
|
||||
then
|
||||
die "Working directory has unstaged changes"
|
||||
fi
|
||||
|
||||
# This is a rough translation of:
|
||||
#
|
||||
# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
|
||||
if git cat-file -e HEAD 2>/dev/null
|
||||
then
|
||||
head=HEAD
|
||||
else
|
||||
head=$(git hash-object -t tree --stdin </dev/null)
|
||||
fi
|
||||
|
||||
if ! git diff-index --quiet --cached --ignore-submodules $head --
|
||||
then
|
||||
die "Working directory has staged changes"
|
||||
fi
|
||||
|
||||
if ! git read-tree -u -m "$commit"
|
||||
then
|
||||
die "Could not update working tree to new HEAD"
|
||||
fi
|
||||
@@ -1,77 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# An example hook script to validate a patch (and/or patch series) before
|
||||
# sending it via email.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an appropriate
|
||||
# message if it wants to prevent the email(s) from being sent.
|
||||
#
|
||||
# To enable this hook, rename this file to "sendemail-validate".
|
||||
#
|
||||
# By default, it will only check that the patch(es) can be applied on top of
|
||||
# the default upstream branch without conflicts in a secondary worktree. After
|
||||
# validation (successful or not) of the last patch of a series, the worktree
|
||||
# will be deleted.
|
||||
#
|
||||
# The following config variables can be set to change the default remote and
|
||||
# remote ref that are used to apply the patches against:
|
||||
#
|
||||
# sendemail.validateRemote (default: origin)
|
||||
# sendemail.validateRemoteRef (default: HEAD)
|
||||
#
|
||||
# Replace the TODO placeholders with appropriate checks according to your
|
||||
# needs.
|
||||
|
||||
validate_cover_letter () {
|
||||
file="$1"
|
||||
# TODO: Replace with appropriate checks (e.g. spell checking).
|
||||
true
|
||||
}
|
||||
|
||||
validate_patch () {
|
||||
file="$1"
|
||||
# Ensure that the patch applies without conflicts.
|
||||
git am -3 "$file" || return
|
||||
# TODO: Replace with appropriate checks for this patch
|
||||
# (e.g. checkpatch.pl).
|
||||
true
|
||||
}
|
||||
|
||||
validate_series () {
|
||||
# TODO: Replace with appropriate checks for the whole series
|
||||
# (e.g. quick build, coding style checks, etc.).
|
||||
true
|
||||
}
|
||||
|
||||
# main -------------------------------------------------------------------------
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
|
||||
then
|
||||
remote=$(git config --default origin --get sendemail.validateRemote) &&
|
||||
ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
|
||||
worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
|
||||
git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
|
||||
git config --replace-all sendemail.validateWorktree "$worktree"
|
||||
else
|
||||
worktree=$(git config --get sendemail.validateWorktree)
|
||||
fi || {
|
||||
echo "sendemail-validate: error: failed to prepare worktree" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
unset GIT_DIR GIT_WORK_TREE
|
||||
cd "$worktree" &&
|
||||
|
||||
if grep -q "^diff --git " "$1"
|
||||
then
|
||||
validate_patch "$1"
|
||||
else
|
||||
validate_cover_letter "$1"
|
||||
fi &&
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
|
||||
then
|
||||
git config --unset-all sendemail.validateWorktree &&
|
||||
trap 'git worktree remove -ff "$worktree"' EXIT &&
|
||||
validate_series
|
||||
fi
|
||||
@@ -1,128 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to block unannotated tags from entering.
|
||||
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
|
||||
#
|
||||
# To enable this hook, rename this file to "update".
|
||||
#
|
||||
# Config
|
||||
# ------
|
||||
# hooks.allowunannotated
|
||||
# This boolean sets whether unannotated tags will be allowed into the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowdeletetag
|
||||
# This boolean sets whether deleting tags will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowmodifytag
|
||||
# This boolean sets whether a tag may be modified after creation. By default
|
||||
# it won't be.
|
||||
# hooks.allowdeletebranch
|
||||
# This boolean sets whether deleting branches will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.denycreatebranch
|
||||
# This boolean sets whether remotely creating branches will be denied
|
||||
# in the repository. By default this is allowed.
|
||||
#
|
||||
|
||||
# --- Command line
|
||||
refname="$1"
|
||||
oldrev="$2"
|
||||
newrev="$3"
|
||||
|
||||
# --- Safety check
|
||||
if [ -z "$GIT_DIR" ]; then
|
||||
echo "Don't run this script from the command line." >&2
|
||||
echo " (if you want, you could supply GIT_DIR then run" >&2
|
||||
echo " $0 <ref> <oldrev> <newrev>)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
|
||||
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Config
|
||||
allowunannotated=$(git config --type=bool hooks.allowunannotated)
|
||||
allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
|
||||
denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
|
||||
allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
|
||||
allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
|
||||
|
||||
# check for no description
|
||||
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
|
||||
case "$projectdesc" in
|
||||
"Unnamed repository"* | "")
|
||||
echo "*** Project description file hasn't been set" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Check types
|
||||
# if $newrev is 0000...0000, it's a commit to delete a ref.
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
if [ "$newrev" = "$zero" ]; then
|
||||
newrev_type=delete
|
||||
else
|
||||
newrev_type=$(git cat-file -t $newrev)
|
||||
fi
|
||||
|
||||
case "$refname","$newrev_type" in
|
||||
refs/tags/*,commit)
|
||||
# un-annotated tag
|
||||
short_refname=${refname##refs/tags/}
|
||||
if [ "$allowunannotated" != "true" ]; then
|
||||
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
|
||||
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,delete)
|
||||
# delete tag
|
||||
if [ "$allowdeletetag" != "true" ]; then
|
||||
echo "*** Deleting a tag is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,tag)
|
||||
# annotated tag
|
||||
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
|
||||
then
|
||||
echo "*** Tag '$refname' already exists." >&2
|
||||
echo "*** Modifying a tag is not allowed in this repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,commit)
|
||||
# branch
|
||||
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
|
||||
echo "*** Creating a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,delete)
|
||||
# delete branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/remotes/*,commit)
|
||||
# tracking branch
|
||||
;;
|
||||
refs/remotes/*,delete)
|
||||
# delete tracking branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Anything else (is there anything else?)
|
||||
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Finished
|
||||
exit 0
|
||||
Binary file not shown.
@@ -1,6 +0,0 @@
|
||||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
@@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 4b027e14574987c7ff329c5ea4980a624f954cad sstent <stuart.stent@gmail.com> 1756500815 -0700 clone: from https://github.com/matin/garth.git
|
||||
@@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 4b027e14574987c7ff329c5ea4980a624f954cad sstent <stuart.stent@gmail.com> 1756500815 -0700 clone: from https://github.com/matin/garth.git
|
||||
@@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 4b027e14574987c7ff329c5ea4980a624f954cad sstent <stuart.stent@gmail.com> 1756500815 -0700 clone: from https://github.com/matin/garth.git
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,47 +0,0 @@
|
||||
# pack-refs with: peeled fully-peeled sorted
|
||||
e88832aab75f7cbdc497d40100a25f7ec9d50302 refs/remotes/origin/add-hydration-support
|
||||
dab68681cede95b96b4a857041255cb2f6042205 refs/remotes/origin/add-training-status
|
||||
b8989b38968d32b4a41bdc9678c4156d6703f454 refs/remotes/origin/coderabbitai/chat/20bTtA
|
||||
e6e142469cb12298129e8fef5772e33a569e96ca refs/remotes/origin/dependabot/github_actions/actions/checkout-5
|
||||
4546623a7ce71e5a87ef202b05c316a2d849efca refs/remotes/origin/hydration
|
||||
4b027e14574987c7ff329c5ea4980a624f954cad refs/remotes/origin/main
|
||||
0e739d308dc2c9a36ad9efc791787340bebb2ec4 refs/remotes/origin/minor-auth-refactor
|
||||
66f488625529bc6a7bafadbb11bf064e30f6850f refs/remotes/origin/refactor/restart
|
||||
19a8fc24d787a6b83b4ac87e6ab9ae8af407a1ee refs/tags/0.4.28
|
||||
bed4a7cd32310c986b1c61a45dd6dfb6c5988fba refs/tags/0.4.29
|
||||
53a166de4cc435b8f694d142f9af4454baada131 refs/tags/0.4.30
|
||||
ca2410a9650c87514a806edbe54e89408acbbe76 refs/tags/0.4.31
|
||||
c77cd9b405082b474914caadeac5cc18f0f6d70e refs/tags/0.4.32
|
||||
2a0dae96d44943edf667f6173479317d2623aa17 refs/tags/0.4.33
|
||||
2d1b21192270e6598ec1326ed0d6017ec23ff057 refs/tags/0.4.34
|
||||
ce5874bbb6492db91a56f76b7d9ad123aa790900 refs/tags/0.4.35
|
||||
5ee41c540b1a2ffd69277ffa99d4dd97a382dbc5 refs/tags/0.4.36
|
||||
515933f709db3b00f5b06d5ba65b267dcda191b9 refs/tags/0.4.37
|
||||
43196b588c553fcc335251d248434002caa0dab0 refs/tags/0.4.38
|
||||
69a1fd4bfa2a697e15e15661eae60f6d9541daf2 refs/tags/0.4.39
|
||||
fad9855b65837d7f5944ae2cd982e4af16b22ff0 refs/tags/0.4.40
|
||||
5aadba6f2e01ae5eb76f2064d4d7e94f2483dbf9 refs/tags/0.4.41
|
||||
6aeb0faaf0d6b473d8dc161373068d2f5413fdfe refs/tags/0.4.42
|
||||
34dc0a162c19c3ec4728517239171164b8009819 refs/tags/0.4.43
|
||||
3a5ebbcdd836bce4b9d5a84191819e24084ff5c7 refs/tags/0.4.44
|
||||
316787d1e3ff69c09725b2eb8ded748a4422abb3 refs/tags/0.4.45
|
||||
ae1b425ea0c7560155ee5e9e2e828fda7c1be43d refs/tags/0.4.46
|
||||
960d8c0ac0b68672e9edc7b9738ba77d60fa806a refs/tags/0.4.47
|
||||
b557b886b229ad304568988cf716c510ef7ecbd7 refs/tags/0.5.0
|
||||
3004df3fb907c81153c9536c142474faf231b698 refs/tags/0.5.1
|
||||
d15e2afd439268ed4c9b4db5a8d75d2afeba7a5d refs/tags/0.5.10
|
||||
ad045ff989456934cb312313b01f0f1ad19af614 refs/tags/0.5.11
|
||||
517eeeda1c6b6504abe7898aa0127f98bbdfc261 refs/tags/0.5.12
|
||||
26b5e2eefdd26b5e5b9bb4b48260a702521cc976 refs/tags/0.5.13
|
||||
922f3c305c71fb177c3bb5e3ca6697ed2e34424c refs/tags/0.5.2
|
||||
a05eb5b25ba8612759e6fe3667b14e26c6af014e refs/tags/0.5.3
|
||||
1a17721e24db7c2fa7f5df2326d9b9919f5de8e5 refs/tags/0.5.4
|
||||
5f27385ecec57b7088f63c9a499b58da5661e904 refs/tags/0.5.5
|
||||
f2592742727e3c95545dec27a7f0781dbdf5d2cd refs/tags/0.5.6
|
||||
11b2ed554611bc3ce488df631d54b54afe097b6e refs/tags/0.5.7
|
||||
10061601252844a18419ecfe0249aa18d3ceb1ab refs/tags/0.5.8
|
||||
2c5b7d7e45bcadbae39e8a4f53169a1614523ea2 refs/tags/0.5.9
|
||||
06b6bf391c1a0a9f0b412057a195415a9dc8755e refs/tags/v0.5.14
|
||||
f58c7e650754e2c45f306f2f707efb389032a2e7 refs/tags/v0.5.15
|
||||
c631afe82ac07302abf499e019e2ebe38c1111ac refs/tags/v0.5.16
|
||||
4b027e14574987c7ff329c5ea4980a624f954cad refs/tags/v0.5.17
|
||||
@@ -1 +0,0 @@
|
||||
4b027e14574987c7ff329c5ea4980a624f954cad
|
||||
@@ -1 +0,0 @@
|
||||
ref: refs/remotes/origin/main
|
||||
1
python-garth/.gitattributes
vendored
1
python-garth/.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
*.ipynb linguist-documentation=true
|
||||
17
python-garth/.github/dependabot.yml
vendored
17
python-garth/.github/dependabot.yml
vendored
@@ -1,17 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "20:00"
|
||||
timezone: "America/Mexico_City"
|
||||
open-pull-requests-limit: 5
|
||||
|
||||
- package-ecosystem: pip
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "20:00"
|
||||
timezone: "America/Mexico_City"
|
||||
open-pull-requests-limit: 5
|
||||
87
python-garth/.github/workflows/ci.yml
vendored
87
python-garth/.github/workflows/ci.yml
vendored
@@ -1,87 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "**"
|
||||
pull_request: {}
|
||||
|
||||
env:
|
||||
COLUMNS: 150
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
checks: write
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
name: lint ${{ matrix.python-version }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv pip install --system -e .
|
||||
uv pip install --system --group linting
|
||||
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
with:
|
||||
extra_args: --all-files --verbose
|
||||
env:
|
||||
SKIP: no-commit-to-branch
|
||||
|
||||
test:
|
||||
name: test ${{ matrix.python-version }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu, macos, windows]
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
|
||||
env:
|
||||
PYTHON: ${{ matrix.python-version }}
|
||||
OS: ${{ matrix.os }}
|
||||
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv pip install --system -e .
|
||||
uv pip install --system --group testing
|
||||
|
||||
- name: test
|
||||
run: make testcov
|
||||
env:
|
||||
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}-with-deps
|
||||
|
||||
- name: upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
files: ./coverage/coverage.xml
|
||||
flags: unittests
|
||||
name: codecov-umbrella
|
||||
fail_ci_if_error: true
|
||||
30
python-garth/.github/workflows/publish.yml
vendored
30
python-garth/.github/workflows/publish.yml
vendored
@@ -1,30 +0,0 @@
|
||||
name: Publish to PyPI
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/garth
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
uv build
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
53
python-garth/.gitignore
vendored
53
python-garth/.gitignore
vendored
@@ -1,53 +0,0 @@
|
||||
# Virtual environments
|
||||
env/
|
||||
env3*/
|
||||
venv/
|
||||
.venv/
|
||||
.envrc
|
||||
.env
|
||||
__pypackages__/
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
|
||||
# Package distribution and build files
|
||||
*.egg-info/
|
||||
dist/
|
||||
/build/
|
||||
_build/
|
||||
|
||||
# Python bytecode and cache files
|
||||
*.py[cod]
|
||||
.cache/
|
||||
/.ghtopdep_cache/
|
||||
.hypothesis
|
||||
.mypy_cache/
|
||||
.pytest_cache/
|
||||
/.ruff_cache/
|
||||
|
||||
# Benchmark and test files
|
||||
/benchmarks/*.json
|
||||
/htmlcov/
|
||||
/codecov.sh
|
||||
/coverage.lcov
|
||||
.coverage
|
||||
test.py
|
||||
/coverage/
|
||||
|
||||
# Documentation files
|
||||
/docs/changelog.md
|
||||
/site/
|
||||
/site.zip
|
||||
|
||||
# Other files and folders
|
||||
.python-version
|
||||
.DS_Store
|
||||
.auto-format
|
||||
/sandbox/
|
||||
/worktrees/
|
||||
.pdm-python
|
||||
tmp/
|
||||
.pdm.toml
|
||||
|
||||
# exclude saved oauth tokens
|
||||
oauth*_token.json
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"MD033": {
|
||||
"allowed_elements": ["img", "a", "source", "picture"]
|
||||
},
|
||||
"MD046": false
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
exclude: '.*\.ipynb$'
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
args: ['--unsafe']
|
||||
- id: check-toml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.6
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies:
|
||||
- tomli
|
||||
exclude: 'cassettes/'
|
||||
|
||||
- repo: https://github.com/DavidAnson/markdownlint-cli2
|
||||
rev: v0.12.1
|
||||
hooks:
|
||||
- id: markdownlint-cli2
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: lint
|
||||
name: lint
|
||||
entry: make lint
|
||||
types: [python]
|
||||
language: system
|
||||
pass_filenames: false
|
||||
@@ -1,21 +0,0 @@
|
||||
# MIT License
|
||||
|
||||
Copyright (c) 2023 Matin Tamizi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,81 +0,0 @@
|
||||
# Based on Makefile for pydantic (github.com/pydantic/pydantic/blob/main/Makefile)
|
||||
|
||||
.DEFAULT_GOAL := all
|
||||
sources = src tests
|
||||
|
||||
.PHONY: .uv ## Check that uv is installed
|
||||
.uv:
|
||||
@uv --version || echo 'Please install uv: https://docs.astral.sh/uv/getting-started/installation/'
|
||||
|
||||
.PHONY: .pre-commit ## Check that pre-commit is installed
|
||||
.pre-commit:
|
||||
@pre-commit -V || echo 'Please install pre-commit: https://pre-commit.com/'
|
||||
|
||||
.PHONY: install ## Install the package, dependencies, and pre-commit for local development
|
||||
install: .uv .pre-commit
|
||||
uv pip install -e .
|
||||
uv pip install --group dev --group linting --group testing
|
||||
pre-commit install --install-hooks
|
||||
|
||||
.PHONY: sync ## Sync dependencies and lockfiles
|
||||
sync: .uv clean
|
||||
uv pip install -e . --force-reinstall
|
||||
uv sync
|
||||
|
||||
.PHONY: format ## Auto-format python source files
|
||||
format: .uv
|
||||
uv run ruff format $(sources)
|
||||
uv run ruff check --fix $(sources)
|
||||
|
||||
.PHONY: lint ## Lint python source files
|
||||
lint: .uv
|
||||
uv run ruff format --check $(sources)
|
||||
uv run ruff check $(sources)
|
||||
uv run mypy $(sources)
|
||||
|
||||
.PHONY: codespell ## Use Codespell to do spellchecking
|
||||
codespell: .pre-commit
|
||||
pre-commit run codespell --all-files
|
||||
|
||||
.PHONY: test ## Run all tests, skipping the type-checker integration tests
|
||||
test: .uv
|
||||
uv run coverage run -m pytest -v --durations=10
|
||||
|
||||
.PHONY: testcov ## Run tests and generate a coverage report, skipping the type-checker integration tests
|
||||
testcov: test
|
||||
@echo "building coverage html"
|
||||
@uv run coverage html
|
||||
@echo "building coverage xml"
|
||||
@uv run coverage xml -o coverage/coverage.xml
|
||||
|
||||
.PHONY: all ## Run the standard set of checks performed in CI
|
||||
all: lint codespell testcov
|
||||
|
||||
.PHONY: clean ## Clear local caches and build artifacts
|
||||
clean:
|
||||
find . -type d -name __pycache__ -exec rm -r {} +
|
||||
find . -type f -name '*.py[co]' -exec rm -f {} +
|
||||
find . -type f -name '*~' -exec rm -f {} +
|
||||
find . -type f -name '.*~' -exec rm -f {} +
|
||||
rm -rf .cache
|
||||
rm -rf .pytest_cache
|
||||
rm -rf .ruff_cache
|
||||
rm -rf htmlcov
|
||||
rm -rf *.egg-info
|
||||
rm -f .coverage
|
||||
rm -f .coverage.*
|
||||
rm -rf build
|
||||
rm -rf dist
|
||||
rm -rf site
|
||||
rm -rf docs/_build
|
||||
rm -rf docs/.changelog.md docs/.version.md docs/.tmp_schema_mappings.html
|
||||
rm -rf fastapi/test.db
|
||||
rm -rf coverage.xml
|
||||
rm -rf __pypackages__ uv.lock
|
||||
|
||||
.PHONY: help ## Display this message
|
||||
help:
|
||||
@grep -E \
|
||||
'^.PHONY: .*?## .*$$' $(MAKEFILE_LIST) | \
|
||||
sort | \
|
||||
awk 'BEGIN {FS = ".PHONY: |## "}; {printf "\033[36m%-19s\033[0m %s\n", $$2, $$3}'
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,89 +0,0 @@
|
||||
[project]
|
||||
name = "garth"
|
||||
dynamic = ["version"]
|
||||
description = "Garmin SSO auth + Connect client"
|
||||
authors = [
|
||||
{name = "Matin Tamizi", email = "mtamizi@duck.com"},
|
||||
]
|
||||
dependencies = [
|
||||
"requests>=2.0.0,<3.0.0",
|
||||
"pydantic>=1.10.12,<3.0.0",
|
||||
"requests-oauthlib>=1.3.1,<3.0.0",
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
"Operating System :: Microsoft :: Windows",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
keywords = ["garmin", "garmin api", "garmin connect", "garmin sso"]
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://github.com/matin/garth"
|
||||
"Repository" = "https://github.com/matin/garth"
|
||||
"Issues" = "https://github.com/matin/garth/issues"
|
||||
"Changelog" = "https://github.com/matin/garth/releases"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.version]
|
||||
path = "src/garth/version.py"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "--ignore=__pypackages__ --ignore-glob=*.yaml"
|
||||
|
||||
[tool.mypy]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 79
|
||||
indent-width = 4
|
||||
target-version = "py310"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I"]
|
||||
ignore = []
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
skip-magic-trailing-comma = false
|
||||
line-ending = "auto"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ipython",
|
||||
"ipdb",
|
||||
"ipykernel",
|
||||
"pandas",
|
||||
"matplotlib",
|
||||
]
|
||||
linting = [
|
||||
"ruff",
|
||||
"mypy",
|
||||
"types-requests",
|
||||
]
|
||||
testing = [
|
||||
"coverage",
|
||||
"pytest",
|
||||
"pytest-vcr",
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["garth"]
|
||||
combine-as-imports = true
|
||||
lines-after-imports = 2
|
||||
|
||||
[project.scripts]
|
||||
garth = "garth.cli:main"
|
||||
@@ -1,59 +0,0 @@
|
||||
from .data import (
|
||||
BodyBatteryData,
|
||||
DailyBodyBatteryStress,
|
||||
HRVData,
|
||||
SleepData,
|
||||
WeightData,
|
||||
)
|
||||
from .http import Client, client
|
||||
from .stats import (
|
||||
DailyHRV,
|
||||
DailyHydration,
|
||||
DailyIntensityMinutes,
|
||||
DailySleep,
|
||||
DailySteps,
|
||||
DailyStress,
|
||||
WeeklyIntensityMinutes,
|
||||
WeeklySteps,
|
||||
WeeklyStress,
|
||||
)
|
||||
from .users import UserProfile, UserSettings
|
||||
from .version import __version__
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BodyBatteryData",
|
||||
"Client",
|
||||
"DailyBodyBatteryStress",
|
||||
"DailyHRV",
|
||||
"DailyHydration",
|
||||
"DailyIntensityMinutes",
|
||||
"DailySleep",
|
||||
"DailySteps",
|
||||
"DailyStress",
|
||||
"HRVData",
|
||||
"SleepData",
|
||||
"WeightData",
|
||||
"UserProfile",
|
||||
"UserSettings",
|
||||
"WeeklyIntensityMinutes",
|
||||
"WeeklySteps",
|
||||
"WeeklyStress",
|
||||
"__version__",
|
||||
"client",
|
||||
"configure",
|
||||
"connectapi",
|
||||
"download",
|
||||
"login",
|
||||
"resume",
|
||||
"save",
|
||||
"upload",
|
||||
]
|
||||
|
||||
configure = client.configure
|
||||
connectapi = client.connectapi
|
||||
download = client.download
|
||||
login = client.login
|
||||
resume = client.load
|
||||
save = client.dump
|
||||
upload = client.upload
|
||||
@@ -1,37 +0,0 @@
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class OAuth1Token:
|
||||
oauth_token: str
|
||||
oauth_token_secret: str
|
||||
mfa_token: str | None = None
|
||||
mfa_expiration_timestamp: datetime | None = None
|
||||
domain: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class OAuth2Token:
|
||||
scope: str
|
||||
jti: str
|
||||
token_type: str
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
expires_in: int
|
||||
expires_at: int
|
||||
refresh_token_expires_in: int
|
||||
refresh_token_expires_at: int
|
||||
|
||||
@property
|
||||
def expired(self):
|
||||
return self.expires_at < time.time()
|
||||
|
||||
@property
|
||||
def refresh_expired(self):
|
||||
return self.refresh_token_expires_at < time.time()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.token_type.title()} {self.access_token}"
|
||||
@@ -1,34 +0,0 @@
|
||||
import argparse
|
||||
import getpass
|
||||
|
||||
import garth
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(prog="garth")
|
||||
parser.add_argument(
|
||||
"--domain",
|
||||
"-d",
|
||||
default="garmin.com",
|
||||
help=(
|
||||
"Domain for Garmin Connect (default: garmin.com). "
|
||||
"Use garmin.cn for China."
|
||||
),
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command")
|
||||
subparsers.add_parser(
|
||||
"login", help="Authenticate with Garmin Connect and print token"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
garth.configure(domain=args.domain)
|
||||
|
||||
match args.command:
|
||||
case "login":
|
||||
email = input("Email: ")
|
||||
password = getpass.getpass("Password: ")
|
||||
garth.login(email, password)
|
||||
token = garth.client.dumps()
|
||||
print(token)
|
||||
case _:
|
||||
parser.print_help()
|
||||
@@ -1,21 +0,0 @@
|
||||
__all__ = [
|
||||
"BodyBatteryData",
|
||||
"BodyBatteryEvent",
|
||||
"BodyBatteryReading",
|
||||
"DailyBodyBatteryStress",
|
||||
"HRVData",
|
||||
"SleepData",
|
||||
"StressReading",
|
||||
"WeightData",
|
||||
]
|
||||
|
||||
from .body_battery import (
|
||||
BodyBatteryData,
|
||||
BodyBatteryEvent,
|
||||
BodyBatteryReading,
|
||||
DailyBodyBatteryStress,
|
||||
StressReading,
|
||||
)
|
||||
from .hrv import HRVData
|
||||
from .sleep import SleepData
|
||||
from .weight import WeightData
|
||||
@@ -1,47 +0,0 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import date
|
||||
from itertools import chain
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from .. import http
|
||||
from ..utils import date_range, format_end_date
|
||||
|
||||
|
||||
MAX_WORKERS = 10
|
||||
|
||||
|
||||
class Data(ABC):
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def get(
|
||||
cls, day: date | str, *, client: http.Client | None = None
|
||||
) -> Self | list[Self] | None: ...
|
||||
|
||||
@classmethod
|
||||
def list(
|
||||
cls,
|
||||
end: date | str | None = None,
|
||||
days: int = 1,
|
||||
*,
|
||||
client: http.Client | None = None,
|
||||
max_workers: int = MAX_WORKERS,
|
||||
) -> list[Self]:
|
||||
client = client or http.client
|
||||
end = format_end_date(end)
|
||||
|
||||
def fetch_date(date_):
|
||||
if day := cls.get(date_, client=client):
|
||||
return day
|
||||
|
||||
dates = date_range(end, days)
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
data = list(executor.map(fetch_date, dates))
|
||||
data = [day for day in data if day is not None]
|
||||
|
||||
return list(
|
||||
chain.from_iterable(
|
||||
day if isinstance(day, list) else [day] for day in data
|
||||
)
|
||||
)
|
||||
@@ -1,11 +0,0 @@
|
||||
__all__ = [
|
||||
"BodyBatteryData",
|
||||
"BodyBatteryEvent",
|
||||
"BodyBatteryReading",
|
||||
"DailyBodyBatteryStress",
|
||||
"StressReading",
|
||||
]
|
||||
|
||||
from .daily_stress import DailyBodyBatteryStress
|
||||
from .events import BodyBatteryData, BodyBatteryEvent
|
||||
from .readings import BodyBatteryReading, StressReading
|
||||
@@ -1,90 +0,0 @@
|
||||
from datetime import date, datetime
|
||||
from functools import cached_property
|
||||
from typing import Any
|
||||
|
||||
from pydantic.dataclasses import dataclass
|
||||
from typing_extensions import Self
|
||||
|
||||
from ... import http
|
||||
from ...utils import camel_to_snake_dict, format_end_date
|
||||
from .._base import Data
|
||||
from .readings import (
|
||||
BodyBatteryReading,
|
||||
StressReading,
|
||||
parse_body_battery_readings,
|
||||
parse_stress_readings,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DailyBodyBatteryStress(Data):
|
||||
"""Complete daily Body Battery and stress data."""
|
||||
|
||||
user_profile_pk: int
|
||||
calendar_date: date
|
||||
start_timestamp_gmt: datetime
|
||||
end_timestamp_gmt: datetime
|
||||
start_timestamp_local: datetime
|
||||
end_timestamp_local: datetime
|
||||
max_stress_level: int
|
||||
avg_stress_level: int
|
||||
stress_chart_value_offset: int
|
||||
stress_chart_y_axis_origin: int
|
||||
stress_values_array: list[list[int]]
|
||||
body_battery_values_array: list[list[Any]]
|
||||
|
||||
@cached_property
|
||||
def body_battery_readings(self) -> list[BodyBatteryReading]:
|
||||
"""Convert body battery values array to structured readings."""
|
||||
return parse_body_battery_readings(self.body_battery_values_array)
|
||||
|
||||
@property
|
||||
def stress_readings(self) -> list[StressReading]:
|
||||
"""Convert stress values array to structured readings."""
|
||||
return parse_stress_readings(self.stress_values_array)
|
||||
|
||||
@property
|
||||
def current_body_battery(self) -> int | None:
|
||||
"""Get the latest Body Battery level."""
|
||||
readings = self.body_battery_readings
|
||||
return readings[-1].level if readings else None
|
||||
|
||||
@property
|
||||
def max_body_battery(self) -> int | None:
|
||||
"""Get the maximum Body Battery level for the day."""
|
||||
readings = self.body_battery_readings
|
||||
return max(reading.level for reading in readings) if readings else None
|
||||
|
||||
@property
|
||||
def min_body_battery(self) -> int | None:
|
||||
"""Get the minimum Body Battery level for the day."""
|
||||
readings = self.body_battery_readings
|
||||
return min(reading.level for reading in readings) if readings else None
|
||||
|
||||
@property
|
||||
def body_battery_change(self) -> int | None:
|
||||
"""Calculate the Body Battery change for the day."""
|
||||
readings = self.body_battery_readings
|
||||
if not readings or len(readings) < 2:
|
||||
return None
|
||||
return readings[-1].level - readings[0].level
|
||||
|
||||
@classmethod
|
||||
def get(
|
||||
cls,
|
||||
day: date | str | None = None,
|
||||
*,
|
||||
client: http.Client | None = None,
|
||||
) -> Self | None:
|
||||
"""Get complete Body Battery and stress data for a specific date."""
|
||||
client = client or http.client
|
||||
date_str = format_end_date(day)
|
||||
|
||||
path = f"/wellness-service/wellness/dailyStress/{date_str}"
|
||||
response = client.connectapi(path)
|
||||
|
||||
if not isinstance(response, dict):
|
||||
return None
|
||||
|
||||
snake_response = camel_to_snake_dict(response)
|
||||
return cls(**snake_response)
|
||||
@@ -1,227 +0,0 @@
|
||||
import logging
|
||||
from datetime import date, datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic.dataclasses import dataclass
|
||||
from typing_extensions import Self
|
||||
|
||||
from ... import http
|
||||
from ...utils import format_end_date
|
||||
from .._base import Data
|
||||
from .readings import BodyBatteryReading, parse_body_battery_readings
|
||||
|
||||
|
||||
MAX_WORKERS = 10
|
||||
|
||||
|
||||
@dataclass
|
||||
class BodyBatteryEvent:
|
||||
"""Body Battery event data."""
|
||||
|
||||
event_type: str
|
||||
event_start_time_gmt: datetime
|
||||
timezone_offset: int
|
||||
duration_in_milliseconds: int
|
||||
body_battery_impact: int
|
||||
feedback_type: str
|
||||
short_feedback: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class BodyBatteryData(Data):
|
||||
"""Legacy Body Battery events data (sleep events only)."""
|
||||
|
||||
event: BodyBatteryEvent | None = None
|
||||
activity_name: str | None = None
|
||||
activity_type: str | None = None
|
||||
activity_id: str | None = None
|
||||
average_stress: float | None = None
|
||||
stress_values_array: list[list[int]] | None = None
|
||||
body_battery_values_array: list[list[Any]] | None = None
|
||||
|
||||
@property
|
||||
def body_battery_readings(self) -> list[BodyBatteryReading]:
|
||||
"""Convert body battery values array to structured readings."""
|
||||
return parse_body_battery_readings(self.body_battery_values_array)
|
||||
|
||||
@property
|
||||
def current_level(self) -> int | None:
|
||||
"""Get the latest Body Battery level."""
|
||||
readings = self.body_battery_readings
|
||||
return readings[-1].level if readings else None
|
||||
|
||||
@property
|
||||
def max_level(self) -> int | None:
|
||||
"""Get the maximum Body Battery level for the day."""
|
||||
readings = self.body_battery_readings
|
||||
return max(reading.level for reading in readings) if readings else None
|
||||
|
||||
@property
|
||||
def min_level(self) -> int | None:
|
||||
"""Get the minimum Body Battery level for the day."""
|
||||
readings = self.body_battery_readings
|
||||
return min(reading.level for reading in readings) if readings else None
|
||||
|
||||
@classmethod
|
||||
def get(
|
||||
cls,
|
||||
date_str: str | date | None = None,
|
||||
*,
|
||||
client: http.Client | None = None,
|
||||
) -> list[Self]:
|
||||
"""Get Body Battery events for a specific date."""
|
||||
client = client or http.client
|
||||
date_str = format_end_date(date_str)
|
||||
|
||||
path = f"/wellness-service/wellness/bodyBattery/events/{date_str}"
|
||||
try:
|
||||
response = client.connectapi(path)
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to fetch Body Battery events: {e}")
|
||||
return []
|
||||
|
||||
if not isinstance(response, list):
|
||||
return []
|
||||
|
||||
events = []
|
||||
for item in response:
|
||||
try:
|
||||
# Parse event data with validation
|
||||
event_data = item.get("event")
|
||||
|
||||
# Validate event_data exists before accessing properties
|
||||
if event_data is None:
|
||||
logging.warning(f"Missing event data in item: {item}")
|
||||
event = None
|
||||
else:
|
||||
# Validate and parse datetime with explicit error handling
|
||||
event_start_time_str = event_data.get("eventStartTimeGmt")
|
||||
if not event_start_time_str:
|
||||
logging.error(
|
||||
f"Missing eventStartTimeGmt in event data: "
|
||||
f"{event_data}"
|
||||
)
|
||||
raise ValueError(
|
||||
"eventStartTimeGmt is required but missing"
|
||||
)
|
||||
|
||||
try:
|
||||
event_start_time_gmt = datetime.fromisoformat(
|
||||
event_start_time_str.replace("Z", "+00:00")
|
||||
)
|
||||
except (ValueError, AttributeError) as e:
|
||||
logging.error(
|
||||
f"Invalid datetime format "
|
||||
f"'{event_start_time_str}': {e}"
|
||||
)
|
||||
raise ValueError(
|
||||
f"Invalid eventStartTimeGmt format: "
|
||||
f"{event_start_time_str}"
|
||||
) from e
|
||||
|
||||
# Validate numeric fields
|
||||
timezone_offset = event_data.get("timezoneOffset", 0)
|
||||
if not isinstance(timezone_offset, (int, float)):
|
||||
logging.warning(
|
||||
f"Invalid timezone_offset type: "
|
||||
f"{type(timezone_offset)}, using 0"
|
||||
)
|
||||
timezone_offset = 0
|
||||
|
||||
duration_ms = event_data.get("durationInMilliseconds", 0)
|
||||
if not isinstance(duration_ms, (int, float)):
|
||||
logging.warning(
|
||||
f"Invalid durationInMilliseconds type: "
|
||||
f"{type(duration_ms)}, using 0"
|
||||
)
|
||||
duration_ms = 0
|
||||
|
||||
battery_impact = event_data.get("bodyBatteryImpact", 0)
|
||||
if not isinstance(battery_impact, (int, float)):
|
||||
logging.warning(
|
||||
f"Invalid bodyBatteryImpact type: "
|
||||
f"{type(battery_impact)}, using 0"
|
||||
)
|
||||
battery_impact = 0
|
||||
|
||||
event = BodyBatteryEvent(
|
||||
event_type=event_data.get("eventType", ""),
|
||||
event_start_time_gmt=event_start_time_gmt,
|
||||
timezone_offset=int(timezone_offset),
|
||||
duration_in_milliseconds=int(duration_ms),
|
||||
body_battery_impact=int(battery_impact),
|
||||
feedback_type=event_data.get("feedbackType", ""),
|
||||
short_feedback=event_data.get("shortFeedback", ""),
|
||||
)
|
||||
|
||||
# Validate data arrays
|
||||
stress_values = item.get("stressValuesArray")
|
||||
if stress_values is not None and not isinstance(
|
||||
stress_values, list
|
||||
):
|
||||
logging.warning(
|
||||
f"Invalid stressValuesArray type: "
|
||||
f"{type(stress_values)}, using None"
|
||||
)
|
||||
stress_values = None
|
||||
|
||||
battery_values = item.get("bodyBatteryValuesArray")
|
||||
if battery_values is not None and not isinstance(
|
||||
battery_values, list
|
||||
):
|
||||
logging.warning(
|
||||
f"Invalid bodyBatteryValuesArray type: "
|
||||
f"{type(battery_values)}, using None"
|
||||
)
|
||||
battery_values = None
|
||||
|
||||
# Validate average_stress
|
||||
avg_stress = item.get("averageStress")
|
||||
if avg_stress is not None and not isinstance(
|
||||
avg_stress, (int, float)
|
||||
):
|
||||
logging.warning(
|
||||
f"Invalid averageStress type: "
|
||||
f"{type(avg_stress)}, using None"
|
||||
)
|
||||
avg_stress = None
|
||||
|
||||
events.append(
|
||||
cls(
|
||||
event=event,
|
||||
activity_name=item.get("activityName"),
|
||||
activity_type=item.get("activityType"),
|
||||
activity_id=item.get("activityId"),
|
||||
average_stress=avg_stress,
|
||||
stress_values_array=stress_values,
|
||||
body_battery_values_array=battery_values,
|
||||
)
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
# Re-raise validation errors with context
|
||||
logging.error(
|
||||
f"Data validation error for Body Battery event item "
|
||||
f"{item}: {e}"
|
||||
)
|
||||
continue
|
||||
except Exception as e:
|
||||
# Log unexpected errors with full context
|
||||
logging.error(
|
||||
f"Unexpected error parsing Body Battery event item "
|
||||
f"{item}: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
continue
|
||||
|
||||
# Log summary of data quality issues
|
||||
total_items = len(response)
|
||||
parsed_events = len(events)
|
||||
if parsed_events < total_items:
|
||||
skipped = total_items - parsed_events
|
||||
logging.info(
|
||||
f"Body Battery events parsing: {parsed_events}/{total_items} "
|
||||
f"successful, {skipped} skipped due to data issues"
|
||||
)
|
||||
|
||||
return events
|
||||
@@ -1,56 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class BodyBatteryReading:
|
||||
"""Individual Body Battery reading."""
|
||||
|
||||
timestamp: int
|
||||
status: str
|
||||
level: int
|
||||
version: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class StressReading:
|
||||
"""Individual stress reading."""
|
||||
|
||||
timestamp: int
|
||||
stress_level: int
|
||||
|
||||
|
||||
def parse_body_battery_readings(
|
||||
body_battery_values_array: list[list[Any]] | None,
|
||||
) -> list[BodyBatteryReading]:
|
||||
"""Convert body battery values array to structured readings."""
|
||||
readings = []
|
||||
for values in body_battery_values_array or []:
|
||||
# Each reading requires 4 values: timestamp, status, level, version
|
||||
if len(values) >= 4:
|
||||
readings.append(
|
||||
BodyBatteryReading(
|
||||
timestamp=values[0],
|
||||
status=values[1],
|
||||
level=values[2],
|
||||
version=values[3],
|
||||
)
|
||||
)
|
||||
# Sort readings by timestamp to ensure chronological order
|
||||
return sorted(readings, key=lambda reading: reading.timestamp)
|
||||
|
||||
|
||||
def parse_stress_readings(
|
||||
stress_values_array: list[list[int]] | None,
|
||||
) -> list[StressReading]:
|
||||
"""Convert stress values array to structured readings."""
|
||||
readings = []
|
||||
for values in stress_values_array or []:
|
||||
# Each reading requires 2 values: timestamp, stress_level
|
||||
if len(values) >= 2:
|
||||
readings.append(
|
||||
StressReading(timestamp=values[0], stress_level=values[1])
|
||||
)
|
||||
# Sort readings by timestamp to ensure chronological order
|
||||
return sorted(readings, key=lambda reading: reading.timestamp)
|
||||
@@ -1,68 +0,0 @@
|
||||
from datetime import date, datetime
|
||||
|
||||
from pydantic.dataclasses import dataclass
|
||||
from typing_extensions import Self
|
||||
|
||||
from .. import http
|
||||
from ..utils import camel_to_snake_dict
|
||||
from ._base import Data
|
||||
|
||||
|
||||
@dataclass
|
||||
class Baseline:
|
||||
low_upper: int
|
||||
balanced_low: int
|
||||
balanced_upper: int
|
||||
marker_value: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class HRVSummary:
|
||||
calendar_date: date
|
||||
weekly_avg: int
|
||||
last_night_avg: int | None
|
||||
last_night_5_min_high: int
|
||||
baseline: Baseline
|
||||
status: str
|
||||
feedback_phrase: str
|
||||
create_time_stamp: datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class HRVReading:
|
||||
hrv_value: int
|
||||
reading_time_gmt: datetime
|
||||
reading_time_local: datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class HRVData(Data):
|
||||
user_profile_pk: int
|
||||
hrv_summary: HRVSummary
|
||||
hrv_readings: list[HRVReading]
|
||||
start_timestamp_gmt: datetime
|
||||
end_timestamp_gmt: datetime
|
||||
start_timestamp_local: datetime
|
||||
end_timestamp_local: datetime
|
||||
sleep_start_timestamp_gmt: datetime
|
||||
sleep_end_timestamp_gmt: datetime
|
||||
sleep_start_timestamp_local: datetime
|
||||
sleep_end_timestamp_local: datetime
|
||||
|
||||
@classmethod
|
||||
def get(
|
||||
cls, day: date | str, *, client: http.Client | None = None
|
||||
) -> Self | None:
|
||||
client = client or http.client
|
||||
path = f"/hrv-service/hrv/{day}"
|
||||
hrv_data = client.connectapi(path)
|
||||
if not hrv_data:
|
||||
return None
|
||||
hrv_data = camel_to_snake_dict(hrv_data)
|
||||
assert isinstance(hrv_data, dict)
|
||||
return cls(**hrv_data)
|
||||
|
||||
@classmethod
|
||||
def list(cls, *args, **kwargs) -> list[Self]:
|
||||
data = super().list(*args, **kwargs)
|
||||
return sorted(data, key=lambda d: d.hrv_summary.calendar_date)
|
||||
@@ -1,123 +0,0 @@
|
||||
from datetime import date, datetime
|
||||
from typing import Optional, Union
|
||||
|
||||
from pydantic.dataclasses import dataclass
|
||||
from typing_extensions import Self
|
||||
|
||||
from .. import http
|
||||
from ..utils import camel_to_snake_dict, get_localized_datetime
|
||||
from ._base import Data
|
||||
|
||||
|
||||
@dataclass
|
||||
class Score:
|
||||
qualifier_key: str
|
||||
optimal_start: Optional[float] = None
|
||||
optimal_end: Optional[float] = None
|
||||
value: Optional[int] = None
|
||||
ideal_start_in_seconds: Optional[float] = None
|
||||
ideal_end_in_seconds: Optional[float] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SleepScores:
|
||||
total_duration: Score
|
||||
stress: Score
|
||||
awake_count: Score
|
||||
overall: Score
|
||||
rem_percentage: Score
|
||||
restlessness: Score
|
||||
light_percentage: Score
|
||||
deep_percentage: Score
|
||||
|
||||
|
||||
@dataclass
|
||||
class DailySleepDTO:
|
||||
id: int
|
||||
user_profile_pk: int
|
||||
calendar_date: date
|
||||
sleep_time_seconds: int
|
||||
nap_time_seconds: int
|
||||
sleep_window_confirmed: bool
|
||||
sleep_window_confirmation_type: str
|
||||
sleep_start_timestamp_gmt: int
|
||||
sleep_end_timestamp_gmt: int
|
||||
sleep_start_timestamp_local: int
|
||||
sleep_end_timestamp_local: int
|
||||
device_rem_capable: bool
|
||||
retro: bool
|
||||
unmeasurable_sleep_seconds: Optional[int] = None
|
||||
deep_sleep_seconds: Optional[int] = None
|
||||
light_sleep_seconds: Optional[int] = None
|
||||
rem_sleep_seconds: Optional[int] = None
|
||||
awake_sleep_seconds: Optional[int] = None
|
||||
sleep_from_device: Optional[bool] = None
|
||||
sleep_version: Optional[int] = None
|
||||
awake_count: Optional[int] = None
|
||||
sleep_scores: Optional[SleepScores] = None
|
||||
auto_sleep_start_timestamp_gmt: Optional[int] = None
|
||||
auto_sleep_end_timestamp_gmt: Optional[int] = None
|
||||
sleep_quality_type_pk: Optional[int] = None
|
||||
sleep_result_type_pk: Optional[int] = None
|
||||
average_sp_o2_value: Optional[float] = None
|
||||
lowest_sp_o2_value: Optional[int] = None
|
||||
highest_sp_o2_value: Optional[int] = None
|
||||
average_sp_o2_hr_sleep: Optional[float] = None
|
||||
average_respiration_value: Optional[float] = None
|
||||
lowest_respiration_value: Optional[float] = None
|
||||
highest_respiration_value: Optional[float] = None
|
||||
avg_sleep_stress: Optional[float] = None
|
||||
age_group: Optional[str] = None
|
||||
sleep_score_feedback: Optional[str] = None
|
||||
sleep_score_insight: Optional[str] = None
|
||||
|
||||
@property
|
||||
def sleep_start(self) -> datetime:
|
||||
return get_localized_datetime(
|
||||
self.sleep_start_timestamp_gmt, self.sleep_start_timestamp_local
|
||||
)
|
||||
|
||||
@property
|
||||
def sleep_end(self) -> datetime:
|
||||
return get_localized_datetime(
|
||||
self.sleep_end_timestamp_gmt, self.sleep_end_timestamp_local
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SleepMovement:
|
||||
start_gmt: datetime
|
||||
end_gmt: datetime
|
||||
activity_level: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class SleepData(Data):
|
||||
daily_sleep_dto: DailySleepDTO
|
||||
sleep_movement: Optional[list[SleepMovement]] = None
|
||||
|
||||
@classmethod
|
||||
def get(
|
||||
cls,
|
||||
day: Union[date, str],
|
||||
*,
|
||||
buffer_minutes: int = 60,
|
||||
client: Optional[http.Client] = None,
|
||||
) -> Optional[Self]:
|
||||
client = client or http.client
|
||||
path = (
|
||||
f"/wellness-service/wellness/dailySleepData/{client.username}?"
|
||||
f"nonSleepBufferMinutes={buffer_minutes}&date={day}"
|
||||
)
|
||||
sleep_data = client.connectapi(path)
|
||||
assert sleep_data
|
||||
sleep_data = camel_to_snake_dict(sleep_data)
|
||||
assert isinstance(sleep_data, dict)
|
||||
return (
|
||||
cls(**sleep_data) if sleep_data["daily_sleep_dto"]["id"] else None
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def list(cls, *args, **kwargs) -> list[Self]:
|
||||
data = super().list(*args, **kwargs)
|
||||
return sorted(data, key=lambda x: x.daily_sleep_dto.calendar_date)
|
||||
@@ -1,81 +0,0 @@
|
||||
from datetime import date, datetime, timedelta
|
||||
from itertools import chain
|
||||
|
||||
from pydantic import Field, ValidationInfo, field_validator
|
||||
from pydantic.dataclasses import dataclass
|
||||
from typing_extensions import Self
|
||||
|
||||
from .. import http
|
||||
from ..utils import (
|
||||
camel_to_snake_dict,
|
||||
format_end_date,
|
||||
get_localized_datetime,
|
||||
)
|
||||
from ._base import MAX_WORKERS, Data
|
||||
|
||||
|
||||
@dataclass
|
||||
class WeightData(Data):
|
||||
sample_pk: int
|
||||
calendar_date: date
|
||||
weight: int
|
||||
source_type: str
|
||||
weight_delta: float
|
||||
timestamp_gmt: int
|
||||
datetime_utc: datetime = Field(..., alias="timestamp_gmt")
|
||||
datetime_local: datetime = Field(..., alias="date")
|
||||
bmi: float | None = None
|
||||
body_fat: float | None = None
|
||||
body_water: float | None = None
|
||||
bone_mass: int | None = None
|
||||
muscle_mass: int | None = None
|
||||
physique_rating: float | None = None
|
||||
visceral_fat: float | None = None
|
||||
metabolic_age: int | None = None
|
||||
|
||||
@field_validator("datetime_local", mode="before")
|
||||
@classmethod
|
||||
def to_localized_datetime(cls, v: int, info: ValidationInfo) -> datetime:
|
||||
return get_localized_datetime(info.data["timestamp_gmt"], v)
|
||||
|
||||
@classmethod
|
||||
def get(
|
||||
cls, day: date | str, *, client: http.Client | None = None
|
||||
) -> Self | None:
|
||||
client = client or http.client
|
||||
path = f"/weight-service/weight/dayview/{day}"
|
||||
data = client.connectapi(path)
|
||||
day_weight_list = data["dateWeightList"] if data else []
|
||||
|
||||
if not day_weight_list:
|
||||
return None
|
||||
|
||||
# Get first (most recent) weight entry for the day
|
||||
weight_data = camel_to_snake_dict(day_weight_list[0])
|
||||
return cls(**weight_data)
|
||||
|
||||
@classmethod
|
||||
def list(
|
||||
cls,
|
||||
end: date | str | None = None,
|
||||
days: int = 1,
|
||||
*,
|
||||
client: http.Client | None = None,
|
||||
max_workers: int = MAX_WORKERS,
|
||||
) -> list[Self]:
|
||||
client = client or http.client
|
||||
end = format_end_date(end)
|
||||
start = end - timedelta(days=days - 1)
|
||||
|
||||
data = client.connectapi(
|
||||
f"/weight-service/weight/range/{start}/{end}?includeAll=true"
|
||||
)
|
||||
weight_summaries = data["dailyWeightSummaries"] if data else []
|
||||
weight_metrics = chain.from_iterable(
|
||||
summary["allWeightMetrics"] for summary in weight_summaries
|
||||
)
|
||||
weight_data_list = (
|
||||
cls(**camel_to_snake_dict(weight_data))
|
||||
for weight_data in weight_metrics
|
||||
)
|
||||
return sorted(weight_data_list, key=lambda d: d.datetime_utc)
|
||||
@@ -1,18 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from requests import HTTPError
|
||||
|
||||
|
||||
@dataclass
|
||||
class GarthException(Exception):
|
||||
"""Base exception for all garth exceptions."""
|
||||
|
||||
msg: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class GarthHTTPError(GarthException):
|
||||
error: HTTPError
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.msg}: {self.error}"
|
||||
@@ -1,247 +0,0 @@
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from typing import IO, Any, Dict, Literal, Tuple
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from requests import HTTPError, Response, Session
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
|
||||
from . import sso
|
||||
from .auth_tokens import OAuth1Token, OAuth2Token
|
||||
from .exc import GarthHTTPError
|
||||
from .utils import asdict
|
||||
|
||||
|
||||
USER_AGENT = {"User-Agent": "GCM-iOS-5.7.2.1"}
|
||||
|
||||
|
||||
class Client:
|
||||
sess: Session
|
||||
last_resp: Response
|
||||
domain: str = "garmin.com"
|
||||
oauth1_token: OAuth1Token | Literal["needs_mfa"] | None = None
|
||||
oauth2_token: OAuth2Token | dict[str, Any] | None = None
|
||||
timeout: int = 10
|
||||
retries: int = 3
|
||||
status_forcelist: Tuple[int, ...] = (408, 429, 500, 502, 503, 504)
|
||||
backoff_factor: float = 0.5
|
||||
pool_connections: int = 10
|
||||
pool_maxsize: int = 10
|
||||
_user_profile: Dict[str, Any] | None = None
|
||||
|
||||
def __init__(self, session: Session | None = None, **kwargs):
|
||||
self.sess = session if session else Session()
|
||||
self.sess.headers.update(USER_AGENT)
|
||||
self.configure(
|
||||
timeout=self.timeout,
|
||||
retries=self.retries,
|
||||
status_forcelist=self.status_forcelist,
|
||||
backoff_factor=self.backoff_factor,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def configure(
|
||||
self,
|
||||
/,
|
||||
oauth1_token: OAuth1Token | None = None,
|
||||
oauth2_token: OAuth2Token | None = None,
|
||||
domain: str | None = None,
|
||||
proxies: Dict[str, str] | None = None,
|
||||
ssl_verify: bool | None = None,
|
||||
timeout: int | None = None,
|
||||
retries: int | None = None,
|
||||
status_forcelist: Tuple[int, ...] | None = None,
|
||||
backoff_factor: float | None = None,
|
||||
pool_connections: int | None = None,
|
||||
pool_maxsize: int | None = None,
|
||||
):
|
||||
if oauth1_token is not None:
|
||||
self.oauth1_token = oauth1_token
|
||||
if oauth2_token is not None:
|
||||
self.oauth2_token = oauth2_token
|
||||
if domain:
|
||||
self.domain = domain
|
||||
if proxies is not None:
|
||||
self.sess.proxies.update(proxies)
|
||||
if ssl_verify is not None:
|
||||
self.sess.verify = ssl_verify
|
||||
if timeout is not None:
|
||||
self.timeout = timeout
|
||||
if retries is not None:
|
||||
self.retries = retries
|
||||
if status_forcelist is not None:
|
||||
self.status_forcelist = status_forcelist
|
||||
if backoff_factor is not None:
|
||||
self.backoff_factor = backoff_factor
|
||||
if pool_connections is not None:
|
||||
self.pool_connections = pool_connections
|
||||
if pool_maxsize is not None:
|
||||
self.pool_maxsize = pool_maxsize
|
||||
|
||||
retry = Retry(
|
||||
total=self.retries,
|
||||
status_forcelist=self.status_forcelist,
|
||||
backoff_factor=self.backoff_factor,
|
||||
)
|
||||
adapter = HTTPAdapter(
|
||||
max_retries=retry,
|
||||
pool_connections=self.pool_connections,
|
||||
pool_maxsize=self.pool_maxsize,
|
||||
)
|
||||
self.sess.mount("https://", adapter)
|
||||
|
||||
@property
|
||||
def user_profile(self):
|
||||
if not self._user_profile:
|
||||
self._user_profile = self.connectapi(
|
||||
"/userprofile-service/socialProfile"
|
||||
)
|
||||
assert isinstance(self._user_profile, dict), (
|
||||
"No profile from connectapi"
|
||||
)
|
||||
return self._user_profile
|
||||
|
||||
@property
|
||||
def profile(self):
|
||||
return self.user_profile
|
||||
|
||||
@property
|
||||
def username(self):
|
||||
return self.user_profile["userName"]
|
||||
|
||||
def request(
|
||||
self,
|
||||
method: str,
|
||||
subdomain: str,
|
||||
path: str,
|
||||
/,
|
||||
api: bool = False,
|
||||
referrer: str | bool = False,
|
||||
headers: dict = {},
|
||||
**kwargs,
|
||||
) -> Response:
|
||||
url = f"https://{subdomain}.{self.domain}"
|
||||
url = urljoin(url, path)
|
||||
if referrer is True and self.last_resp:
|
||||
headers["referer"] = self.last_resp.url
|
||||
if api:
|
||||
assert self.oauth1_token, (
|
||||
"OAuth1 token is required for API requests"
|
||||
)
|
||||
if (
|
||||
not isinstance(self.oauth2_token, OAuth2Token)
|
||||
or self.oauth2_token.expired
|
||||
):
|
||||
self.refresh_oauth2()
|
||||
headers["Authorization"] = str(self.oauth2_token)
|
||||
self.last_resp = self.sess.request(
|
||||
method,
|
||||
url,
|
||||
headers=headers,
|
||||
timeout=self.timeout,
|
||||
**kwargs,
|
||||
)
|
||||
try:
|
||||
self.last_resp.raise_for_status()
|
||||
except HTTPError as e:
|
||||
raise GarthHTTPError(
|
||||
msg="Error in request",
|
||||
error=e,
|
||||
)
|
||||
return self.last_resp
|
||||
|
||||
def get(self, *args, **kwargs) -> Response:
|
||||
return self.request("GET", *args, **kwargs)
|
||||
|
||||
def post(self, *args, **kwargs) -> Response:
|
||||
return self.request("POST", *args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs) -> Response:
|
||||
return self.request("DELETE", *args, **kwargs)
|
||||
|
||||
def put(self, *args, **kwargs) -> Response:
|
||||
return self.request("PUT", *args, **kwargs)
|
||||
|
||||
def login(self, *args, **kwargs):
|
||||
self.oauth1_token, self.oauth2_token = sso.login(
|
||||
*args, **kwargs, client=self
|
||||
)
|
||||
return self.oauth1_token, self.oauth2_token
|
||||
|
||||
def resume_login(self, *args, **kwargs):
|
||||
self.oauth1_token, self.oauth2_token = sso.resume_login(
|
||||
*args, **kwargs
|
||||
)
|
||||
return self.oauth1_token, self.oauth2_token
|
||||
|
||||
def refresh_oauth2(self):
|
||||
assert self.oauth1_token and isinstance(
|
||||
self.oauth1_token, OAuth1Token
|
||||
), "OAuth1 token is required for OAuth2 refresh"
|
||||
# There is a way to perform a refresh of an OAuth2 token, but it
|
||||
# appears even Garmin uses this approach when the OAuth2 is expired
|
||||
self.oauth2_token = sso.exchange(self.oauth1_token, self)
|
||||
|
||||
def connectapi(
|
||||
self, path: str, method="GET", **kwargs
|
||||
) -> Dict[str, Any] | None:
|
||||
resp = self.request(method, "connectapi", path, api=True, **kwargs)
|
||||
if resp.status_code == 204:
|
||||
return None
|
||||
return resp.json()
|
||||
|
||||
def download(self, path: str, **kwargs) -> bytes:
|
||||
resp = self.get("connectapi", path, api=True, **kwargs)
|
||||
return resp.content
|
||||
|
||||
def upload(
|
||||
self, fp: IO[bytes], /, path: str = "/upload-service/upload"
|
||||
) -> Dict[str, Any]:
|
||||
fname = os.path.basename(fp.name)
|
||||
files = {"file": (fname, fp)}
|
||||
result = self.connectapi(
|
||||
path,
|
||||
method="POST",
|
||||
files=files,
|
||||
)
|
||||
assert result is not None, "No result from upload"
|
||||
return result
|
||||
|
||||
def dump(self, dir_path: str):
|
||||
dir_path = os.path.expanduser(dir_path)
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
with open(os.path.join(dir_path, "oauth1_token.json"), "w") as f:
|
||||
if self.oauth1_token:
|
||||
json.dump(asdict(self.oauth1_token), f, indent=4)
|
||||
with open(os.path.join(dir_path, "oauth2_token.json"), "w") as f:
|
||||
if self.oauth2_token:
|
||||
json.dump(asdict(self.oauth2_token), f, indent=4)
|
||||
|
||||
def dumps(self) -> str:
|
||||
r = []
|
||||
r.append(asdict(self.oauth1_token))
|
||||
r.append(asdict(self.oauth2_token))
|
||||
s = json.dumps(r)
|
||||
return base64.b64encode(s.encode()).decode()
|
||||
|
||||
def load(self, dir_path: str):
|
||||
dir_path = os.path.expanduser(dir_path)
|
||||
with open(os.path.join(dir_path, "oauth1_token.json")) as f:
|
||||
oauth1 = OAuth1Token(**json.load(f))
|
||||
with open(os.path.join(dir_path, "oauth2_token.json")) as f:
|
||||
oauth2 = OAuth2Token(**json.load(f))
|
||||
self.configure(
|
||||
oauth1_token=oauth1, oauth2_token=oauth2, domain=oauth1.domain
|
||||
)
|
||||
|
||||
def loads(self, s: str):
|
||||
oauth1, oauth2 = json.loads(base64.b64decode(s))
|
||||
self.configure(
|
||||
oauth1_token=OAuth1Token(**oauth1),
|
||||
oauth2_token=OAuth2Token(**oauth2),
|
||||
domain=oauth1.get("domain"),
|
||||
)
|
||||
|
||||
|
||||
client = Client()
|
||||
@@ -1,259 +0,0 @@
|
||||
import asyncio
|
||||
import re
|
||||
import time
|
||||
from typing import Any, Callable, Dict, Literal, Tuple
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
import requests
|
||||
from requests import Session
|
||||
from requests_oauthlib import OAuth1Session
|
||||
|
||||
from . import http
|
||||
from .auth_tokens import OAuth1Token, OAuth2Token
|
||||
from .exc import GarthException
|
||||
|
||||
|
||||
CSRF_RE = re.compile(r'name="_csrf"\s+value="(.+?)"')
|
||||
TITLE_RE = re.compile(r"<title>(.+?)</title>")
|
||||
OAUTH_CONSUMER_URL = "https://thegarth.s3.amazonaws.com/oauth_consumer.json"
|
||||
OAUTH_CONSUMER: Dict[str, str] = {}
|
||||
USER_AGENT = {"User-Agent": "com.garmin.android.apps.connectmobile"}
|
||||
|
||||
|
||||
class GarminOAuth1Session(OAuth1Session):
|
||||
def __init__(
|
||||
self,
|
||||
/,
|
||||
parent: Session | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
global OAUTH_CONSUMER
|
||||
if not OAUTH_CONSUMER:
|
||||
OAUTH_CONSUMER = requests.get(OAUTH_CONSUMER_URL).json()
|
||||
super().__init__(
|
||||
OAUTH_CONSUMER["consumer_key"],
|
||||
OAUTH_CONSUMER["consumer_secret"],
|
||||
**kwargs,
|
||||
)
|
||||
if parent is not None:
|
||||
self.mount("https://", parent.adapters["https://"])
|
||||
self.proxies = parent.proxies
|
||||
self.verify = parent.verify
|
||||
|
||||
|
||||
def login(
|
||||
email: str,
|
||||
password: str,
|
||||
/,
|
||||
client: "http.Client | None" = None,
|
||||
prompt_mfa: Callable | None = lambda: input("MFA code: "),
|
||||
return_on_mfa: bool = False,
|
||||
) -> (
|
||||
Tuple[OAuth1Token, OAuth2Token]
|
||||
| Tuple[Literal["needs_mfa"], dict[str, Any]]
|
||||
):
|
||||
"""Login to Garmin Connect.
|
||||
|
||||
Args:
|
||||
email: Garmin account email
|
||||
password: Garmin account password
|
||||
client: Optional HTTP client to use
|
||||
prompt_mfa: Callable that prompts for MFA code. Returns on MFA if None.
|
||||
return_on_mfa: If True, returns dict with MFA info instead of prompting
|
||||
|
||||
Returns:
|
||||
If return_on_mfa=False (default):
|
||||
Tuple[OAuth1Token, OAuth2Token]: OAuth tokens after login
|
||||
If return_on_mfa=True and MFA required:
|
||||
dict: Contains needs_mfa and client_state for resume_login()
|
||||
"""
|
||||
client = client or http.client
|
||||
|
||||
# Define params based on domain
|
||||
SSO = f"https://sso.{client.domain}/sso"
|
||||
SSO_EMBED = f"{SSO}/embed"
|
||||
SSO_EMBED_PARAMS = dict(
|
||||
id="gauth-widget",
|
||||
embedWidget="true",
|
||||
gauthHost=SSO,
|
||||
)
|
||||
SIGNIN_PARAMS = {
|
||||
**SSO_EMBED_PARAMS,
|
||||
**dict(
|
||||
gauthHost=SSO_EMBED,
|
||||
service=SSO_EMBED,
|
||||
source=SSO_EMBED,
|
||||
redirectAfterAccountLoginUrl=SSO_EMBED,
|
||||
redirectAfterAccountCreationUrl=SSO_EMBED,
|
||||
),
|
||||
}
|
||||
|
||||
# Set cookies
|
||||
client.get("sso", "/sso/embed", params=SSO_EMBED_PARAMS)
|
||||
|
||||
# Get CSRF token
|
||||
client.get(
|
||||
"sso",
|
||||
"/sso/signin",
|
||||
params=SIGNIN_PARAMS,
|
||||
referrer=True,
|
||||
)
|
||||
csrf_token = get_csrf_token(client.last_resp.text)
|
||||
|
||||
# Submit login form with email and password
|
||||
client.post(
|
||||
"sso",
|
||||
"/sso/signin",
|
||||
params=SIGNIN_PARAMS,
|
||||
referrer=True,
|
||||
data=dict(
|
||||
username=email,
|
||||
password=password,
|
||||
embed="true",
|
||||
_csrf=csrf_token,
|
||||
),
|
||||
)
|
||||
title = get_title(client.last_resp.text)
|
||||
|
||||
# Handle MFA
|
||||
if "MFA" in title:
|
||||
if return_on_mfa or prompt_mfa is None:
|
||||
return "needs_mfa", {
|
||||
"signin_params": SIGNIN_PARAMS,
|
||||
"client": client,
|
||||
}
|
||||
|
||||
handle_mfa(client, SIGNIN_PARAMS, prompt_mfa)
|
||||
title = get_title(client.last_resp.text)
|
||||
|
||||
if title != "Success":
|
||||
raise GarthException(f"Unexpected title: {title}")
|
||||
return _complete_login(client)
|
||||
|
||||
|
||||
def get_oauth1_token(ticket: str, client: "http.Client") -> OAuth1Token:
|
||||
sess = GarminOAuth1Session(parent=client.sess)
|
||||
base_url = f"https://connectapi.{client.domain}/oauth-service/oauth/"
|
||||
login_url = f"https://sso.{client.domain}/sso/embed"
|
||||
url = (
|
||||
f"{base_url}preauthorized?ticket={ticket}&login-url={login_url}"
|
||||
"&accepts-mfa-tokens=true"
|
||||
)
|
||||
resp = sess.get(
|
||||
url,
|
||||
headers=USER_AGENT,
|
||||
timeout=client.timeout,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
parsed = parse_qs(resp.text)
|
||||
token = {k: v[0] for k, v in parsed.items()}
|
||||
return OAuth1Token(domain=client.domain, **token) # type: ignore
|
||||
|
||||
|
||||
def exchange(oauth1: OAuth1Token, client: "http.Client") -> OAuth2Token:
|
||||
sess = GarminOAuth1Session(
|
||||
resource_owner_key=oauth1.oauth_token,
|
||||
resource_owner_secret=oauth1.oauth_token_secret,
|
||||
parent=client.sess,
|
||||
)
|
||||
data = dict(mfa_token=oauth1.mfa_token) if oauth1.mfa_token else {}
|
||||
base_url = f"https://connectapi.{client.domain}/oauth-service/oauth/"
|
||||
url = f"{base_url}exchange/user/2.0"
|
||||
headers = {
|
||||
**USER_AGENT,
|
||||
**{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}
|
||||
resp = sess.post(
|
||||
url,
|
||||
headers=headers,
|
||||
data=data,
|
||||
timeout=client.timeout,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
token = resp.json()
|
||||
return OAuth2Token(**set_expirations(token))
|
||||
|
||||
|
||||
def handle_mfa(
|
||||
client: "http.Client", signin_params: dict, prompt_mfa: Callable
|
||||
) -> None:
|
||||
csrf_token = get_csrf_token(client.last_resp.text)
|
||||
if asyncio.iscoroutinefunction(prompt_mfa):
|
||||
mfa_code = asyncio.run(prompt_mfa())
|
||||
else:
|
||||
mfa_code = prompt_mfa()
|
||||
client.post(
|
||||
"sso",
|
||||
"/sso/verifyMFA/loginEnterMfaCode",
|
||||
params=signin_params,
|
||||
referrer=True,
|
||||
data={
|
||||
"mfa-code": mfa_code,
|
||||
"embed": "true",
|
||||
"_csrf": csrf_token,
|
||||
"fromPage": "setupEnterMfaCode",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def set_expirations(token: dict) -> dict:
|
||||
token["expires_at"] = int(time.time() + token["expires_in"])
|
||||
token["refresh_token_expires_at"] = int(
|
||||
time.time() + token["refresh_token_expires_in"]
|
||||
)
|
||||
return token
|
||||
|
||||
|
||||
def get_csrf_token(html: str) -> str:
|
||||
m = CSRF_RE.search(html)
|
||||
if not m:
|
||||
raise GarthException("Couldn't find CSRF token")
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def get_title(html: str) -> str:
|
||||
m = TITLE_RE.search(html)
|
||||
if not m:
|
||||
raise GarthException("Couldn't find title")
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def resume_login(
|
||||
client_state: dict, mfa_code: str
|
||||
) -> Tuple[OAuth1Token, OAuth2Token]:
|
||||
"""Complete login after MFA code is provided.
|
||||
|
||||
Args:
|
||||
client_state: The client state from login() when MFA was needed
|
||||
mfa_code: The MFA code provided by the user
|
||||
|
||||
Returns:
|
||||
Tuple[OAuth1Token, OAuth2Token]: The OAuth tokens after login
|
||||
"""
|
||||
client = client_state["client"]
|
||||
signin_params = client_state["signin_params"]
|
||||
handle_mfa(client, signin_params, lambda: mfa_code)
|
||||
return _complete_login(client)
|
||||
|
||||
|
||||
def _complete_login(client: "http.Client") -> Tuple[OAuth1Token, OAuth2Token]:
|
||||
"""Complete the login process after successful authentication.
|
||||
|
||||
Args:
|
||||
client: The HTTP client
|
||||
|
||||
Returns:
|
||||
Tuple[OAuth1Token, OAuth2Token]: The OAuth tokens
|
||||
"""
|
||||
# Parse ticket
|
||||
m = re.search(r'embed\?ticket=([^"]+)"', client.last_resp.text)
|
||||
if not m:
|
||||
raise GarthException(
|
||||
"Couldn't find ticket in response"
|
||||
) # pragma: no cover
|
||||
ticket = m.group(1)
|
||||
|
||||
oauth1 = get_oauth1_token(ticket, client)
|
||||
oauth2 = exchange(oauth1, client)
|
||||
|
||||
return oauth1, oauth2
|
||||
@@ -1,18 +0,0 @@
|
||||
__all__ = [
|
||||
"DailyHRV",
|
||||
"DailyHydration",
|
||||
"DailyIntensityMinutes",
|
||||
"DailySleep",
|
||||
"DailySteps",
|
||||
"DailyStress",
|
||||
"WeeklyIntensityMinutes",
|
||||
"WeeklyStress",
|
||||
"WeeklySteps",
|
||||
]
|
||||
|
||||
from .hrv import DailyHRV
|
||||
from .hydration import DailyHydration
|
||||
from .intensity_minutes import DailyIntensityMinutes, WeeklyIntensityMinutes
|
||||
from .sleep import DailySleep
|
||||
from .steps import DailySteps, WeeklySteps
|
||||
from .stress import DailyStress, WeeklyStress
|
||||
@@ -1,53 +0,0 @@
|
||||
from datetime import date, timedelta
|
||||
from typing import ClassVar
|
||||
|
||||
from pydantic.dataclasses import dataclass
|
||||
from typing_extensions import Self
|
||||
|
||||
from .. import http
|
||||
from ..utils import camel_to_snake_dict, format_end_date
|
||||
|
||||
|
||||
@dataclass
|
||||
class Stats:
|
||||
calendar_date: date
|
||||
|
||||
_path: ClassVar[str]
|
||||
_page_size: ClassVar[int]
|
||||
|
||||
@classmethod
|
||||
def list(
|
||||
cls,
|
||||
end: date | str | None = None,
|
||||
period: int = 1,
|
||||
*,
|
||||
client: http.Client | None = None,
|
||||
) -> list[Self]:
|
||||
client = client or http.client
|
||||
end = format_end_date(end)
|
||||
period_type = "days" if "daily" in cls._path else "weeks"
|
||||
|
||||
if period > cls._page_size:
|
||||
page = cls.list(end, cls._page_size, client=client)
|
||||
if not page:
|
||||
return []
|
||||
page = (
|
||||
cls.list(
|
||||
end - timedelta(**{period_type: cls._page_size}),
|
||||
period - cls._page_size,
|
||||
client=client,
|
||||
)
|
||||
+ page
|
||||
)
|
||||
return page
|
||||
|
||||
start = end - timedelta(**{period_type: period - 1})
|
||||
path = cls._path.format(start=start, end=end, period=period)
|
||||
page_dirs = client.connectapi(path)
|
||||
if not isinstance(page_dirs, list) or not page_dirs:
|
||||
return []
|
||||
page_dirs = [d for d in page_dirs if isinstance(d, dict)]
|
||||
if page_dirs and "values" in page_dirs[0]:
|
||||
page_dirs = [{**stat, **stat.pop("values")} for stat in page_dirs]
|
||||
page_dirs = [camel_to_snake_dict(stat) for stat in page_dirs]
|
||||
return [cls(**stat) for stat in page_dirs]
|
||||
@@ -1,66 +0,0 @@
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Any, ClassVar, cast
|
||||
|
||||
from pydantic.dataclasses import dataclass
|
||||
from typing_extensions import Self
|
||||
|
||||
from .. import http
|
||||
from ..utils import camel_to_snake_dict, format_end_date
|
||||
|
||||
|
||||
@dataclass
|
||||
class HRVBaseline:
|
||||
low_upper: int
|
||||
balanced_low: int
|
||||
balanced_upper: int
|
||||
marker_value: float | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class DailyHRV:
|
||||
calendar_date: date
|
||||
weekly_avg: int | None
|
||||
last_night_avg: int | None
|
||||
last_night_5_min_high: int | None
|
||||
baseline: HRVBaseline | None
|
||||
status: str
|
||||
feedback_phrase: str
|
||||
create_time_stamp: datetime
|
||||
|
||||
_path: ClassVar[str] = "/hrv-service/hrv/daily/{start}/{end}"
|
||||
_page_size: ClassVar[int] = 28
|
||||
|
||||
@classmethod
|
||||
def list(
|
||||
cls,
|
||||
end: date | str | None = None,
|
||||
period: int = 28,
|
||||
*,
|
||||
client: http.Client | None = None,
|
||||
) -> list[Self]:
|
||||
client = client or http.client
|
||||
end = format_end_date(end)
|
||||
|
||||
# Paginate if period is greater than page size
|
||||
if period > cls._page_size:
|
||||
page = cls.list(end, cls._page_size, client=client)
|
||||
if not page:
|
||||
return []
|
||||
page = (
|
||||
cls.list(
|
||||
end - timedelta(days=cls._page_size),
|
||||
period - cls._page_size,
|
||||
client=client,
|
||||
)
|
||||
+ page
|
||||
)
|
||||
return page
|
||||
|
||||
start = end - timedelta(days=period - 1)
|
||||
path = cls._path.format(start=start, end=end)
|
||||
response = client.connectapi(path)
|
||||
if response is None:
|
||||
return []
|
||||
daily_hrv = camel_to_snake_dict(response)["hrv_summaries"]
|
||||
daily_hrv = cast(list[dict[str, Any]], daily_hrv)
|
||||
return [cls(**hrv) for hrv in daily_hrv]
|
||||
@@ -1,17 +0,0 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
from ._base import Stats
|
||||
|
||||
|
||||
BASE_PATH = "/usersummary-service/stats/hydration"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DailyHydration(Stats):
|
||||
value_in_ml: float
|
||||
goal_in_ml: float
|
||||
|
||||
_path: ClassVar[str] = f"{BASE_PATH}/daily/{{start}}/{{end}}"
|
||||
_page_size: ClassVar[int] = 28
|
||||
@@ -1,28 +0,0 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
from ._base import Stats
|
||||
|
||||
|
||||
BASE_PATH = "/usersummary-service/stats/im"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DailyIntensityMinutes(Stats):
|
||||
weekly_goal: int
|
||||
moderate_value: int | None = None
|
||||
vigorous_value: int | None = None
|
||||
|
||||
_path: ClassVar[str] = f"{BASE_PATH}/daily/{{start}}/{{end}}"
|
||||
_page_size: ClassVar[int] = 28
|
||||
|
||||
|
||||
@dataclass
|
||||
class WeeklyIntensityMinutes(Stats):
|
||||
weekly_goal: int
|
||||
moderate_value: int | None = None
|
||||
vigorous_value: int | None = None
|
||||
|
||||
_path: ClassVar[str] = f"{BASE_PATH}/weekly/{{start}}/{{end}}"
|
||||
_page_size: ClassVar[int] = 52
|
||||
@@ -1,15 +0,0 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
from ._base import Stats
|
||||
|
||||
|
||||
@dataclass
|
||||
class DailySleep(Stats):
|
||||
value: int | None
|
||||
|
||||
_path: ClassVar[str] = (
|
||||
"/wellness-service/stats/daily/sleep/score/{start}/{end}"
|
||||
)
|
||||
_page_size: ClassVar[int] = 28
|
||||
@@ -1,30 +0,0 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
from ._base import Stats
|
||||
|
||||
|
||||
BASE_PATH = "/usersummary-service/stats/steps"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DailySteps(Stats):
|
||||
total_steps: int | None
|
||||
total_distance: int | None
|
||||
step_goal: int
|
||||
|
||||
_path: ClassVar[str] = f"{BASE_PATH}/daily/{{start}}/{{end}}"
|
||||
_page_size: ClassVar[int] = 28
|
||||
|
||||
|
||||
@dataclass
|
||||
class WeeklySteps(Stats):
|
||||
total_steps: int
|
||||
average_steps: float
|
||||
average_distance: float
|
||||
total_distance: float
|
||||
wellness_data_days_count: int
|
||||
|
||||
_path: ClassVar[str] = f"{BASE_PATH}/weekly/{{end}}/{{period}}"
|
||||
_page_size: ClassVar[int] = 52
|
||||
@@ -1,28 +0,0 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
from ._base import Stats
|
||||
|
||||
|
||||
BASE_PATH = "/usersummary-service/stats/stress"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DailyStress(Stats):
|
||||
overall_stress_level: int
|
||||
rest_stress_duration: int | None = None
|
||||
low_stress_duration: int | None = None
|
||||
medium_stress_duration: int | None = None
|
||||
high_stress_duration: int | None = None
|
||||
|
||||
_path: ClassVar[str] = f"{BASE_PATH}/daily/{{start}}/{{end}}"
|
||||
_page_size: ClassVar[int] = 28
|
||||
|
||||
|
||||
@dataclass
|
||||
class WeeklyStress(Stats):
|
||||
value: int
|
||||
|
||||
_path: ClassVar[str] = f"{BASE_PATH}/weekly/{{end}}/{{period}}"
|
||||
_page_size: ClassVar[int] = 52
|
||||
@@ -1,5 +0,0 @@
|
||||
from .profile import UserProfile
|
||||
from .settings import UserSettings
|
||||
|
||||
|
||||
__all__ = ["UserProfile", "UserSettings"]
|
||||
@@ -1,79 +0,0 @@
|
||||
from pydantic.dataclasses import dataclass
|
||||
from typing_extensions import Self
|
||||
|
||||
from .. import http
|
||||
from ..utils import camel_to_snake_dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserProfile:
|
||||
id: int
|
||||
profile_id: int
|
||||
garmin_guid: str
|
||||
display_name: str
|
||||
full_name: str
|
||||
user_name: str
|
||||
profile_image_type: str | None
|
||||
profile_image_url_large: str | None
|
||||
profile_image_url_medium: str | None
|
||||
profile_image_url_small: str | None
|
||||
location: str | None
|
||||
facebook_url: str | None
|
||||
twitter_url: str | None
|
||||
personal_website: str | None
|
||||
motivation: str | None
|
||||
bio: str | None
|
||||
primary_activity: str | None
|
||||
favorite_activity_types: list[str]
|
||||
running_training_speed: float
|
||||
cycling_training_speed: float
|
||||
favorite_cycling_activity_types: list[str]
|
||||
cycling_classification: str | None
|
||||
cycling_max_avg_power: float
|
||||
swimming_training_speed: float
|
||||
profile_visibility: str
|
||||
activity_start_visibility: str
|
||||
activity_map_visibility: str
|
||||
course_visibility: str
|
||||
activity_heart_rate_visibility: str
|
||||
activity_power_visibility: str
|
||||
badge_visibility: str
|
||||
show_age: bool
|
||||
show_weight: bool
|
||||
show_height: bool
|
||||
show_weight_class: bool
|
||||
show_age_range: bool
|
||||
show_gender: bool
|
||||
show_activity_class: bool
|
||||
show_vo_2_max: bool
|
||||
show_personal_records: bool
|
||||
show_last_12_months: bool
|
||||
show_lifetime_totals: bool
|
||||
show_upcoming_events: bool
|
||||
show_recent_favorites: bool
|
||||
show_recent_device: bool
|
||||
show_recent_gear: bool
|
||||
show_badges: bool
|
||||
other_activity: str | None
|
||||
other_primary_activity: str | None
|
||||
other_motivation: str | None
|
||||
user_roles: list[str]
|
||||
name_approved: bool
|
||||
user_profile_full_name: str
|
||||
make_golf_scorecards_private: bool
|
||||
allow_golf_live_scoring: bool
|
||||
allow_golf_scoring_by_connections: bool
|
||||
user_level: int
|
||||
user_point: int
|
||||
level_update_date: str
|
||||
level_is_viewed: bool
|
||||
level_point_threshold: int
|
||||
user_point_offset: int
|
||||
user_pro: bool
|
||||
|
||||
@classmethod
|
||||
def get(cls, /, client: http.Client | None = None) -> Self:
|
||||
client = client or http.client
|
||||
profile = client.connectapi("/userprofile-service/socialProfile")
|
||||
assert isinstance(profile, dict)
|
||||
return cls(**camel_to_snake_dict(profile))
|
||||
@@ -1,108 +0,0 @@
|
||||
from datetime import date
|
||||
from typing import Dict
|
||||
|
||||
from pydantic.dataclasses import dataclass
|
||||
from typing_extensions import Self
|
||||
|
||||
from .. import http
|
||||
from ..utils import camel_to_snake_dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class PowerFormat:
|
||||
format_id: int
|
||||
format_key: str
|
||||
min_fraction: int
|
||||
max_fraction: int
|
||||
grouping_used: bool
|
||||
display_format: str | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class FirstDayOfWeek:
|
||||
day_id: int
|
||||
day_name: str
|
||||
sort_order: int
|
||||
is_possible_first_day: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class WeatherLocation:
|
||||
use_fixed_location: bool | None
|
||||
latitude: float | None
|
||||
longitude: float | None
|
||||
location_name: str | None
|
||||
iso_country_code: str | None
|
||||
postal_code: str | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserData:
|
||||
gender: str
|
||||
weight: float
|
||||
height: float
|
||||
time_format: str
|
||||
birth_date: date
|
||||
measurement_system: str
|
||||
activity_level: str | None
|
||||
handedness: str
|
||||
power_format: PowerFormat
|
||||
heart_rate_format: PowerFormat
|
||||
first_day_of_week: FirstDayOfWeek
|
||||
vo_2_max_running: float | None
|
||||
vo_2_max_cycling: float | None
|
||||
lactate_threshold_speed: float | None
|
||||
lactate_threshold_heart_rate: float | None
|
||||
dive_number: int | None
|
||||
intensity_minutes_calc_method: str
|
||||
moderate_intensity_minutes_hr_zone: int
|
||||
vigorous_intensity_minutes_hr_zone: int
|
||||
hydration_measurement_unit: str
|
||||
hydration_containers: list[Dict[str, float | str | None]]
|
||||
hydration_auto_goal_enabled: bool
|
||||
firstbeat_max_stress_score: float | None
|
||||
firstbeat_cycling_lt_timestamp: int | None
|
||||
firstbeat_running_lt_timestamp: int | None
|
||||
threshold_heart_rate_auto_detected: bool
|
||||
ftp_auto_detected: bool | None
|
||||
training_status_paused_date: str | None
|
||||
weather_location: WeatherLocation | None
|
||||
golf_distance_unit: str | None
|
||||
golf_elevation_unit: str | None
|
||||
golf_speed_unit: str | None
|
||||
external_bottom_time: float | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserSleep:
|
||||
sleep_time: int
|
||||
default_sleep_time: bool
|
||||
wake_time: int
|
||||
default_wake_time: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserSleepWindow:
|
||||
sleep_window_frequency: str
|
||||
start_sleep_time_seconds_from_midnight: int
|
||||
end_sleep_time_seconds_from_midnight: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserSettings:
|
||||
id: int
|
||||
user_data: UserData
|
||||
user_sleep: UserSleep
|
||||
connect_date: str | None
|
||||
source_type: str | None
|
||||
user_sleep_windows: list[UserSleepWindow] | None = None
|
||||
|
||||
@classmethod
|
||||
def get(cls, /, client: http.Client | None = None) -> Self:
|
||||
client = client or http.client
|
||||
settings = client.connectapi(
|
||||
"/userprofile-service/userprofile/user-settings"
|
||||
)
|
||||
assert isinstance(settings, dict)
|
||||
data = camel_to_snake_dict(settings)
|
||||
return cls(**data)
|
||||
@@ -1,73 +0,0 @@
|
||||
import dataclasses
|
||||
import re
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
|
||||
CAMEL_TO_SNAKE = re.compile(
|
||||
r"((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z])|(?<=[a-zA-Z])[0-9])"
|
||||
)
|
||||
|
||||
|
||||
def camel_to_snake(camel_str: str) -> str:
|
||||
snake_str = CAMEL_TO_SNAKE.sub(r"_\1", camel_str)
|
||||
return snake_str.lower()
|
||||
|
||||
|
||||
def camel_to_snake_dict(camel_dict: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Converts a dictionary's keys from camel case to snake case. This version
|
||||
handles nested dictionaries and lists.
|
||||
"""
|
||||
snake_dict: Dict[str, Any] = {}
|
||||
for k, v in camel_dict.items():
|
||||
new_key = camel_to_snake(k)
|
||||
if isinstance(v, dict):
|
||||
snake_dict[new_key] = camel_to_snake_dict(v)
|
||||
elif isinstance(v, list):
|
||||
snake_dict[new_key] = [
|
||||
camel_to_snake_dict(i) if isinstance(i, dict) else i for i in v
|
||||
]
|
||||
else:
|
||||
snake_dict[new_key] = v
|
||||
return snake_dict
|
||||
|
||||
|
||||
def format_end_date(end: Union[date, str, None]) -> date:
|
||||
if end is None:
|
||||
end = date.today()
|
||||
elif isinstance(end, str):
|
||||
end = date.fromisoformat(end)
|
||||
return end
|
||||
|
||||
|
||||
def date_range(date_: Union[date, str], days: int):
|
||||
date_ = date_ if isinstance(date_, date) else date.fromisoformat(date_)
|
||||
for day in range(days):
|
||||
yield date_ - timedelta(days=day)
|
||||
|
||||
|
||||
def asdict(obj):
|
||||
if dataclasses.is_dataclass(obj):
|
||||
result = {}
|
||||
for field in dataclasses.fields(obj):
|
||||
value = getattr(obj, field.name)
|
||||
result[field.name] = asdict(value)
|
||||
return result
|
||||
|
||||
if isinstance(obj, List):
|
||||
return [asdict(v) for v in obj]
|
||||
|
||||
if isinstance(obj, (datetime, date)):
|
||||
return obj.isoformat()
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def get_localized_datetime(
|
||||
gmt_timestamp: int, local_timestamp: int
|
||||
) -> datetime:
|
||||
local_diff = local_timestamp - gmt_timestamp
|
||||
local_offset = timezone(timedelta(milliseconds=local_diff))
|
||||
gmt_time = datetime.fromtimestamp(gmt_timestamp / 1000, timezone.utc)
|
||||
return gmt_time.astimezone(local_offset)
|
||||
@@ -1 +0,0 @@
|
||||
__version__ = "0.5.17"
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1,65 +0,0 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Authorization:
|
||||
- Bearer SANITIZED
|
||||
Connection:
|
||||
- keep-alive
|
||||
User-Agent:
|
||||
- Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15
|
||||
(KHTML, like Gecko) Mobile/15E148
|
||||
referer:
|
||||
- https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
|
||||
method: GET
|
||||
uri: https://connectapi.garmin.com/usersummary-service/stats/stress/daily/2023-07-21/2023-07-21
|
||||
response:
|
||||
body:
|
||||
string: '[{"calendarDate": "2023-07-21", "values": {"highStressDuration": 3240,
|
||||
"lowStressDuration": 20280, "overallStressLevel": 35, "restStressDuration":
|
||||
31020, "mediumStressDuration": 11640}}]'
|
||||
headers:
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-RAY:
|
||||
- 7f12d932aa00b6ee-QRO
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Fri, 04 Aug 2023 00:57:49 GMT
|
||||
NEL:
|
||||
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=FuGLLTTuU8CV4eTRQnQ7XY0oTrHoXEaIYrPbxrkK1vRVT4yAr2Zv0YIj4D%2BZ0eQTeYgycpuCP1gSE4yk0bZE2Aj2p29AIZ2Ce%2BuOUJqB9Mp54VyHR9uEC5AAcVLUYqtzpE4YIK0Fgw%3D%3D"}],"group":"cf-nel","max_age":604800}'
|
||||
Server:
|
||||
- cloudflare
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cache-control:
|
||||
- no-cache, no-store, private
|
||||
pragma:
|
||||
- no-cache
|
||||
set-cookie:
|
||||
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- SameSite=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -1,223 +0,0 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Authorization:
|
||||
- Bearer SANITIZED
|
||||
Connection:
|
||||
- keep-alive
|
||||
User-Agent:
|
||||
- Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15
|
||||
(KHTML, like Gecko) Mobile/15E148
|
||||
method: GET
|
||||
uri: https://connectapi.garmin.com/activity-service/activity/12135235656
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAA6VW23LbNhD9lQ6eRZsXURL5ZstOookle2SpmbbT8ayAJYUGBDgAqETJ+N874EWk
|
||||
ZbV96Btx9oLF7p5d/iRALT9we1wwkgZhEMVhFE/iyegk2G4XdyT9SaqKM5KS2SxJqD9NvCQJY288
|
||||
zSIPwmnosSBE5tNxsIMpee3NV1AgSclvKgcyIpVB/aRVxgW6C8M4CSZ+OCLcLCth+XOptH0CjdKS
|
||||
NANhsHe0OZZ4t3l0odhjWZsHk2hUHz7jkaTk2NxR1vabVidMnPdPnDGUJ58ajdWcWmQnyGpeFLAT
|
||||
2CKvI4KH1s/Ztcnw0kpSsJgrzX8gIyNilLaPmqEmaeDXeaBozFxJq5VYV+LcWTh0Vmp+AIsuf5YX
|
||||
+LuSuJXctiaV5LZ+dhyO6kNjdFOg5hSu53tOIVdkRDKgVmmS+ld+7+iC5uuIFGiBgYX2Cm4eNc+5
|
||||
BEFSqyscEYYHTvGmLAWnYLmSC2ksCNF8u2yEQTh2/ZKjtP+iJyshWq1nq7nMO8g1wwelC7Augqz+
|
||||
cgbTUXtonplxW/eVMYpysMjmqtIGe9cCjN2WDCzeuRymJPTDyPMTL0w2YZCOkzSOr3zXhKVQwJD9
|
||||
h9qBM1RbLTr/ezBPShwFl3jqmj2Y+R60vQMLXcb2YD7pDS9wIV3ezQB/Ut/wraj143ixkJm6s6qu
|
||||
dE+Tp68DmjBuSgFH2XCqsFDwH9wVvBKiBZdgufxl00nKlmwF5LjV4gF0jt2DzmRLZLwqSEr21pYm
|
||||
vb420RUU8ENJ+GauqCquc9AFlx5VUiK1XqkVu26dvHDnxVxPZjEECSYeTHfojSEIvGSXJV4GFHwa
|
||||
syALwGvfc1XK/H2IzwUI8X+jiPww88PA85FOvTGjibfzaeQxF9yOxuDP6FkUbcp79tM9F2zBDEn/
|
||||
+LM93QxGUYsblEbp9nCapXUUDVaArBwfK+1GAvl4s14uVsSV8oCrqtg5tOvfcq4qN/mCYZt/Ufqr
|
||||
qmzf59zc2LILpQMbmi7Rwl3P5gZ0liSKwiSZziazgHTK7hWuu6LJdOZ34K+oDVfS4Q2xX+vGXUiL
|
||||
0rinSYv6AMIMKfBcCm57BG/5V1zC9xtjuLFLxVwumjBr0S1Yi/q4NdD34lCwxgK4HEyIWth7q4my
|
||||
eXzgxg64ua5kTa8vXLKGjo2IgkDJQN+7ae5MO0GuVVWuOcNmw7Xalb40WwqwdI9sfllaojZKglgj
|
||||
VbpfKVBZNQdB5yCU5gO2Z3BQmtt+jLgmAdHXtFtTZ0+6l25Bna7N6V9++HaBDRYaCjzUw3eutMbB
|
||||
rnsdEVMVBehj2ybGgrZuKj0o6gZ/PxFnmyBJAz+Np/VEPCl+XG7eDk7fH6gxlyWuJEmTq8l0MiKF
|
||||
OnCZ353DKKA0yN7h9JSvwC2wXaH7DNYrDQ6oIcdPa5LOpg4o4Ht9SAbSDRYlanDEI2k0btUuoVxe
|
||||
QK1uevA+y5Da7mIJqNWO081F6SXZEk3T52T1+HJzv368Xcxfbu9X9x8Wm5dgRv7R6VvD1bmpS3Q9
|
||||
b/AZHfeiEfkGFvW9sbyAutrB+3c8wA5dhberz6vHLysy+L1q9R4UsPo9syAI4mkYTuJJGNdZ6vrz
|
||||
Acp3Natb49lqNIakk8jdjJKdgDo/jGcZapQUO9yrFY3F0pXWlS4/mYRtxYZOX1//BraydeSxCgAA
|
||||
headers:
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-RAY:
|
||||
- 80e771592928359a-DFW
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json;charset=UTF-8
|
||||
Date:
|
||||
- Fri, 29 Sep 2023 21:50:37 GMT
|
||||
NEL:
|
||||
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=m77%2F5qqWH%2FzNYg9h9aJB23Sa8erERimKhptV3iEpuPvFpQKcBvr8kHp%2B0tcMmTnLbEN%2FZr0zE7r9yfH0C5bHKK80P8CeBzFhzo9RkFicBPRHZMMaxBDwn7fNmDGgZpOGV9NydCV2LQ%3D%3D"}],"group":"cf-nel","max_age":604800}'
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- SameSite=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cache-control:
|
||||
- no-cache, no-store, private
|
||||
pragma:
|
||||
- no-cache
|
||||
set-cookie:
|
||||
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- SameSite=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Authorization:
|
||||
- Bearer SANITIZED
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '0'
|
||||
Cookie:
|
||||
- _cfuvid=SANITIZED; ADRUM_BT1=SANITIZED; ADRUM_BTa=SANITIZED; SameSite=SANITIZED
|
||||
User-Agent:
|
||||
- Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15
|
||||
(KHTML, like Gecko) Mobile/15E148
|
||||
method: DELETE
|
||||
uri: https://connectapi.garmin.com/activity-service/activity/12135235656
|
||||
response:
|
||||
body:
|
||||
string: ''
|
||||
headers:
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-RAY:
|
||||
- 80e7715a5b05359a-DFW
|
||||
Connection:
|
||||
- keep-alive
|
||||
Date:
|
||||
- Fri, 29 Sep 2023 21:50:37 GMT
|
||||
NEL:
|
||||
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=RR3a8akw5KIfJ40HMjm%2FVxtacqIHiN3EkPNr5ZFwq02kvcv2Wt8fzZL9kbXMXFTHMd3iL7ZcPj4074wQQsCMR29xUXurv6SH5Nd2hdW2qeQT%2Bl7fsosUtcPp3mglfZcBnFOy9JteAg%3D%3D"}],"group":"cf-nel","max_age":604800}'
|
||||
Server:
|
||||
- cloudflare
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cache-control:
|
||||
- no-cache, no-store, private
|
||||
pragma:
|
||||
- no-cache
|
||||
set-cookie:
|
||||
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- SameSite=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
status:
|
||||
code: 204
|
||||
message: No Content
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Authorization:
|
||||
- Bearer SANITIZED
|
||||
Connection:
|
||||
- keep-alive
|
||||
Cookie:
|
||||
- _cfuvid=SANITIZED; ADRUM_BT1=SANITIZED; ADRUM_BTa=SANITIZED; SameSite=SANITIZED
|
||||
User-Agent:
|
||||
- Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15
|
||||
(KHTML, like Gecko) Mobile/15E148
|
||||
method: GET
|
||||
uri: https://connectapi.garmin.com/activity-service/activity/12135235656
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAA6tWyk0tLk5MT1WyUvIICQlQMDEwUfDLL1Fwyy/NS1HSUUotKsovUrJS8ssvAQu5
|
||||
ViSnFpRk5ucp1QIAv2CADDwAAAA=
|
||||
headers:
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-RAY:
|
||||
- 80e7715cbe4b359a-DFW
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json;charset=UTF-8
|
||||
Date:
|
||||
- Fri, 29 Sep 2023 21:50:37 GMT
|
||||
NEL:
|
||||
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=sBhGq8nTIKEsQsz%2FTdQHQGlCFN93mqZLF20y8BX8Vf4lPeqs0bM27QSt8IU1udH7S7x8wGmhmS3PzVMmthvCEbT8L1GrNICizdJ6H28Z%2Bd3F%2B4Em9Upz9aThxIiFzIPB8Zw6iEN%2Fqg%3D%3D"}],"group":"cf-nel","max_age":604800}'
|
||||
Server:
|
||||
- cloudflare
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cache-control:
|
||||
- no-cache, no-store, private
|
||||
pragma:
|
||||
- no-cache
|
||||
set-cookie:
|
||||
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- SameSite=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
status:
|
||||
code: 404
|
||||
message: Not Found
|
||||
version: 1
|
||||
@@ -1,618 +0,0 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Authorization:
|
||||
- Bearer SANITIZED
|
||||
Connection:
|
||||
- keep-alive
|
||||
User-Agent:
|
||||
- Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15
|
||||
(KHTML, like Gecko) Mobile/15E148
|
||||
referer:
|
||||
- https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
|
||||
method: GET
|
||||
uri: https://connectapi.garmin.com/download-service/files/activity/11998957007
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
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
|
||||
6ujSg9z1Je76Y9z1w1263KVHurTnO993AX1tvt/O9/J8j9Ie7vv5vuvEt7a4rr7r5rsC3/Xy/SN8
|
||||
v7fvjvS9Ab430HeDfTo3pZMMbi1Sk2sYT9LncjnEY873EhN874l+vnfej7bh6+m7op03q7kH3P4r
|
||||
Pun9YePx9YV/C/9/8lrZ5KZ7930RPjvfD7/sCZ7v5Q6i9Vjo5Xv9tuAnnclblZd7Ij3PW44vr5a+
|
||||
vIC/udrsoDYnqM0Nauk76xXUHIFvTr6wAUEtfWEnBbUXFCeWBbXLA3dp4FYE7rLArQzSlwfpVUHt
|
||||
VUHt5qB2R1Dzy2DNr4I132PLk6+2s0t3cemuLt3NpQtcutCle7v0kS7dx6X7uvRRLj3QpUtc+hiX
|
||||
Ptalp7t0pUvPdelTXXqeSy916fNcutqlL3LptEvXuHStS9/m0ne69FaXvt+ld7r0wy79c5f+hUs/
|
||||
4tJPufTTLv0CrzrP971M38vi0V1opTlZad1918N3Rb43yPcG+16574/0/Rk8aY87zXcLfHeO7xb5
|
||||
brHvlvjuXN+d77uLff8S37/CuSudu8F3a53b5Pu7fe9J33vGd8/67jnfPe+7l31X77s9vtvru3a8
|
||||
nvUndg3/dwGtQ/7ZhbuE0P+P//CPnzv3wsVn/jt7pX/1L9xeWh/f5tm/+//yfeynyHMHJ7bktLT0
|
||||
dy0tjwTrcpsyz/dy/NxBqTPo77zT6tGz9TXXIBBtbYV4o4EtLfNa/uO7ivy2u4ppnnekl/MddoqZ
|
||||
QS3ts+gXl+F7AW8NLkG7rXAvyD8h+sUU0h7l6Kb/BVBLBwipDEp8X3oAAJRYAQBQSwECFAAUAAgI
|
||||
CACQkytXqQxKfF96AACUWAEAGAAAAAAAAAAAAAAAAAAAAAAAMTE5OTg5NTcwMDdfQUNUSVZJVFku
|
||||
Zml0UEsFBgAAAAABAAEARgAAAKV6AAAAAA==
|
||||
headers:
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-RAY:
|
||||
- 8051f899de562845-DFW
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- application/x-zip-compressed
|
||||
Date:
|
||||
- Mon, 11 Sep 2023 18:28:32 GMT
|
||||
NEL:
|
||||
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=UasM5X17vbczyPuHS8ZkKgf9dhIaPVvfztkmUlVZCFpeDvl304Gx8EwjapAM4eMIjt70PTgSNnNAMmXtzkVKh0BVUVAUf9X3p6ro5v%2FIN2mLHmxnv3AU27akiMmY8QOJmwHsSrIsqQ%3D%3D"}],"group":"cf-nel","max_age":604800}'
|
||||
Server:
|
||||
- cloudflare
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cache-control:
|
||||
- no-cache, no-store, private
|
||||
content-disposition:
|
||||
- attachment; filename="11998957007.zip"
|
||||
pragma:
|
||||
- no-cache
|
||||
set-cookie:
|
||||
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- SameSite=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED;
|
||||
Secure
|
||||
- _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -1,105 +0,0 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Connection:
|
||||
- keep-alive
|
||||
User-Agent:
|
||||
- python-requests/2.31.0
|
||||
method: GET
|
||||
uri: https://thegarth.s3.amazonaws.com/oauth_consumer.json
|
||||
response:
|
||||
body:
|
||||
string: '{"consumer_key": "SANITIZED", "consumer_secret": "SANITIZED"}'
|
||||
headers:
|
||||
Accept-Ranges:
|
||||
- bytes
|
||||
Content-Length:
|
||||
- '124'
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Fri, 04 Aug 2023 03:43:24 GMT
|
||||
ETag:
|
||||
- '"20240b1013cb35419bb5b2cff1407a4e"'
|
||||
Last-Modified:
|
||||
- Thu, 03 Aug 2023 00:16:11 GMT
|
||||
Server:
|
||||
- AmazonS3
|
||||
x-amz-id-2:
|
||||
- V8hHVVhXCEX7RD7Vzw8IsKS//xFr7co0468z4G834xsWIJ46GpXAwZKETm68Odczy470cauMZXo=
|
||||
x-amz-request-id:
|
||||
- Z03APPY9GXZFWZ69
|
||||
x-amz-server-side-encryption:
|
||||
- AES256
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: ''
|
||||
headers:
|
||||
Accept:
|
||||
- !!binary |
|
||||
Ki8q
|
||||
Accept-Encoding:
|
||||
- !!binary |
|
||||
Z3ppcCwgZGVmbGF0ZQ==
|
||||
Authorization:
|
||||
- Bearer SANITIZED
|
||||
Connection:
|
||||
- !!binary |
|
||||
a2VlcC1hbGl2ZQ==
|
||||
Content-Length:
|
||||
- !!binary |
|
||||
MA==
|
||||
Content-Type:
|
||||
- !!binary |
|
||||
YXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVk
|
||||
User-Agent:
|
||||
- !!binary |
|
||||
Y29tLmdhcm1pbi5hbmRyb2lkLmFwcHMuY29ubmVjdG1vYmlsZQ==
|
||||
method: POST
|
||||
uri: https://connectapi.garmin.com/oauth-service/oauth/exchange/user/2.0
|
||||
response:
|
||||
body:
|
||||
string: '{"scope": "COMMUNITY_COURSE_READ GARMINPAY_WRITE GOLF_API_READ ATP_READ
|
||||
GHS_SAMD GHS_UPLOAD INSIGHTS_READ COMMUNITY_COURSE_WRITE CONNECT_WRITE GCOFFER_WRITE
|
||||
GARMINPAY_READ DT_CLIENT_ANALYTICS_WRITE GOLF_API_WRITE INSIGHTS_WRITE PRODUCT_SEARCH_READ
|
||||
GCOFFER_READ CONNECT_READ ATP_WRITE", "jti": "SANITIZED", "access_token":
|
||||
"SANITIZED", "token_type": "Bearer", "refresh_token": "SANITIZED", "expires_in":
|
||||
107182, "refresh_token_expires_in": 2591999}'
|
||||
headers:
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-RAY:
|
||||
- 7f13cbbc2a754790-DFW
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Fri, 04 Aug 2023 03:43:23 GMT
|
||||
NEL:
|
||||
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=T5EHGPEATgD5SbyAMCZh1mKSEJkUest3sa7l%2FTpQ6dZl3uv3K%2BW7Ng20XTseNh3KPdqYzHdkCCB5d4npBML1ZgAAmVUYdkrYiM2uJhmn7WfvSdrIyme0uCf9p5t7RY6%2BRUxNYfhL8Q%3D%3D"}],"group":"cf-nel","max_age":604800}'
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cache-control:
|
||||
- no-cache, no-store, private
|
||||
pragma:
|
||||
- no-cache
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,601 +0,0 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Connection:
|
||||
- keep-alive
|
||||
User-Agent:
|
||||
- Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15
|
||||
(KHTML, like Gecko) Mobile/15E148
|
||||
method: GET
|
||||
uri: https://sso.garmin.com/sso/embed?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso
|
||||
response:
|
||||
body:
|
||||
string: "<html>\n\t<head>\n\t <title>GAuth Embedded Version</title>\n\t <meta
|
||||
http-equiv=\"X-UA-Compatible\" content=\"IE=edge;\" />\n\t <style type=\"text/css\">\n\t
|
||||
\ \t#gauth-widget {border: none !important;}\n\t </style>\n\t</head>\n\t<body>\n\t\t<script
|
||||
type=\"text/javascript\" src=\"/sso/js/jquery/3.1.1/jquery.min.js?20210319\"></script>\n\n<div>\n\t<pre>\n\t<span>ERROR:
|
||||
clientId parameter must be specified!!!</span>\n\n\t<span >Usage: https://sso.garmin.com/sso/embed?clientId=<clientId>&locale=<locale>...</span>\n\n\tRequest
|
||||
parameter configuration options:\n\n\tNAME REQ VALUES
|
||||
\ DESCRIPTION\n\t------------------
|
||||
\ --- -------------------------------------------------------
|
||||
\ ---------------------------------------------------------------------------------------------------\n\tclientId
|
||||
\ Yes \"MY_GARMIN\"/\"BUY_GARMIN\"/\"FLY_GARMIN\"/ Client
|
||||
identifier for your web application\n\t \"RMA\"/\"GarminConnect\"/\"OpenCaching\"/etc\n\tlocale
|
||||
\ Yes \"en\", \"bg\", \"cs\", \"da\", \"de\", \"es\",
|
||||
\"el\", \"fr\", \"hr\", User's current locale, to display the GAuth login
|
||||
widget internationalized properly.\n\t \"in\",
|
||||
\"it\", \"iw\", \"hu\", \"ms\", \"nb\", \"nl\", \"no\", \"pl\", (All the
|
||||
currently supported locales are listed in the Values section.)\n\t \"pt\",
|
||||
\"pt_BR\", \"ru\", \"sk\", \"sl\", \"fi\", \"sv\", \"tr\",\n\t \"uk\",
|
||||
\"th\", \"ja\", \"ko\", \"zh_TW\", \"zh\", \"vi_VN\"\n\tcssUrl No
|
||||
\ Absolute URL to custom CSS file. Use custom CSS
|
||||
styling for the GAuth login widget.\n\treauth No
|
||||
\ true/false (Default value is false) Specify true if
|
||||
you want to ensure that the GAuth login widget shows up,\n\t even
|
||||
if the SSO infrastructure remembers the user and would immediately log them
|
||||
in.\n\t This
|
||||
is useful if you know a user is logged on, but want a different user to be
|
||||
allowed to logon.\n\tinitialFocus No true/false (Default
|
||||
value is true) If you don't want the GAuth login widget
|
||||
to autofocus in it's \"Email or Username\" field upon initial loading,\n\t
|
||||
\ then
|
||||
specify this option and set it to false.\n\trememberMeShown No
|
||||
\ true/false (Default value is false) Whether the \"Remember
|
||||
Me\" check box is shown in the GAuth login widget.\n\trememberMeChecked No
|
||||
\ true/false (Default value is false) Whether the \"Remember
|
||||
Me\" check box feature is checked by default.\n\tcreateAccountShown No
|
||||
\ true/false (Default value is true) Whether the \"Don't
|
||||
have an account? Create One\" link is shown in the GAuth login widget.\n\tsocialEnabled
|
||||
\ No true/false (Default value is false) If
|
||||
set to false, do not show any social sign in elements or allow social sign
|
||||
ins.\n\tlockToEmailAddress No Email address to pre-load and
|
||||
lock. If specified, the specified email address will
|
||||
be pre-loaded in the main \"Email\" field in the SSO login form,\n\t as
|
||||
well as in in the \"Email Address\" field in the \"Forgot Password?\" password
|
||||
reset form,\n\t and
|
||||
both fields will be disabled so they can't be changed.\n\t (If
|
||||
for some reason you want to force re-authentications for a known customer
|
||||
account, you can make use of this option.)\n\topenCreateAccount No
|
||||
\ true/false (Default value is false) If set to true,
|
||||
immediately display the the account creation screen.\n\tdisplayNameShown No
|
||||
\ true/false (Default value is false) If set to true,
|
||||
show the \"Display Name\" field on the account creation screen, to allow the
|
||||
user\n\t to
|
||||
set their central MyGarmin display name upon account creation.\n\tglobalOptInShown
|
||||
\ No true/false (Default value is false) Whether
|
||||
the \"Global Opt-In\" check box is shown on the create account & create social
|
||||
account screens.\n\t If
|
||||
set to true these screens will show a \"Sign Up For Email\" check box with
|
||||
accompanying text\n\t \"I
|
||||
would also like to receive email about promotions and new products.\"\n\t
|
||||
\ If
|
||||
checked, the Customer 2.0 account that is created will have it's global opt-in
|
||||
flag set to true,\n\t and
|
||||
Garmin email communications will be allowed.\n\tglobalOptInChecked No
|
||||
\ true/false (Default value is false) Whether the \"Global
|
||||
Opt-In\" check box is checked by default.\n\tconsumeServiceTicket No
|
||||
\ true/false (Default value is true) IF you don't specify
|
||||
a redirectAfterAccountLoginUrl AND you set this to false, the GAuth login
|
||||
widget\n\t will
|
||||
NOT consume the service ticket assigned and will not seamlessly log you into
|
||||
your webapp.\n\t It
|
||||
will send a SUCCESS JavaScript event with the service ticket and service url
|
||||
you can take\n\t and
|
||||
explicitly validate against the SSO infrastructure yourself.\n\t (By
|
||||
using casClient's SingleSignOnUtils.authenticateServiceTicket() utility method,\n\t
|
||||
\ or
|
||||
calling web service customerWebServices_v1.2 AccountManagementService.authenticateServiceTicket().)\n\tmobile
|
||||
\ No true/false (Default value is false) Setting
|
||||
to true will cause mobile friendly views to be shown instead of the tradition
|
||||
screens.\n\ttermsOfUseUrl No Absolute URL to your custom
|
||||
terms of use URL. If not specified, defaults to http://www.garmin.com/terms\n\tprivacyStatementUrl
|
||||
\ No Absolute URL to your custom privacy statement URL. If
|
||||
not specified, defaults to http://www.garmin.com/privacy\n\tproductSupportUrl
|
||||
\ No Absolute URL to your custom product support URL. If
|
||||
not specified, defaults to http://www.garmin.com/us/support/contact\n\tgenerateExtraServiceTicket
|
||||
\ No true/false (Default value is false) If set
|
||||
to true, generate an extra unconsumed service ticket.\n\t\t (The
|
||||
service ticket validation response will include the extra service ticket.)\n\tgenerateTwoExtraServiceTickets
|
||||
\ No true/false (Default value is false) If set to true,
|
||||
generate two extra unconsumed service tickets.\n\t\t\t\t\t\t\t\t\t \t\t\t
|
||||
\ (The service ticket validation response will include the extra service
|
||||
tickets.)\n\tgenerateNoServiceTicket No true/false (Default value
|
||||
is false) If you don't want SSO to generate a service
|
||||
ticket at all when logging in to the GAuth login widget.\n (Useful
|
||||
when allowing logins to static sites that are not SSO enabled and can't consume
|
||||
the service ticket.)\n\tconnectLegalTerms No true/false (Default
|
||||
value is false) Whether to show the connectLegalTerms
|
||||
on the create account page\n\tshowTermsOfUse No true/false
|
||||
(Default value is false) Whether to show the showTermsOfUse
|
||||
on the create account page\n\tshowPrivacyPolicy No true/false
|
||||
(Default value is false) Whether to show the showPrivacyPolicy
|
||||
on the create account page\n\tshowConnectLegalAge No true/false
|
||||
(Default value is false) Whether to show the showConnectLegalAge
|
||||
on the create account page\n\tlocationPromptShown No true/false
|
||||
(Default value is false) If set to true, ask the customer
|
||||
during account creation to verify their country of residence.\n\tshowPassword
|
||||
\ No true/false (Default value is true) If
|
||||
set to false, mobile version for createAccount and login screens would hide
|
||||
the password\n\tuseCustomHeader No true/false (Default value
|
||||
is false) If set to true, the \"Sign in\" text will be
|
||||
replaced by custom text. Contact CDS team to set the i18n text for your client
|
||||
id.\n\tmfaRequired No true/false (Default value is false)
|
||||
\ Require multi factor authentication for all authenticating
|
||||
users.\n\tperformMFACheck No true/false (Default value is
|
||||
false) If set to true, ask the logged in user to pass
|
||||
a multi factor authentication check. (Only valid for an already logged in
|
||||
user.)\n\trememberMyBrowserShown No true/false (Default value is
|
||||
false) Whether the \"Remember My Browser\" check box
|
||||
is shown in the GAuth login widget MFA verification screen.\n\trememberMyBrowserChecked
|
||||
\ No true/false (Default value is false) Whether
|
||||
the \"Remember My Browser\" check box feature is checked by default.\n\tconsentTypeIds\t\t\t\t\tNo\tconsent_types
|
||||
ids\t\t \t\t\t\t\t\t\t\t multiple consent types ids can be passed as consentTypeIds=type1&consentTypeIds=type2\n\t</pre>\n</div>\n\n\n\t<script>(function(){var
|
||||
js = \"window['__CF$cv$params']={r:'7f1ab8a0eff61559'};_cpo=document.createElement('script');_cpo.nonce='',_cpo.src='/cdn-cgi/challenge-platform/scripts/invisible.js',document.getElementsByTagName('head')[0].appendChild(_cpo);\";var
|
||||
_0xh = document.createElement('iframe');_0xh.height = 1;_0xh.width = 1;_0xh.style.position
|
||||
= 'absolute';_0xh.style.top = 0;_0xh.style.left = 0;_0xh.style.border = 'none';_0xh.style.visibility
|
||||
= 'hidden';document.body.appendChild(_0xh);function handler() {var _0xi =
|
||||
_0xh.contentDocument || _0xh.contentWindow.document;if (_0xi) {var _0xj =
|
||||
_0xi.createElement('script');_0xj.innerHTML = js;_0xi.getElementsByTagName('head')[0].appendChild(_0xj);}}if
|
||||
(document.readyState !== 'loading') {handler();} else if (window.addEventListener)
|
||||
{document.addEventListener('DOMContentLoaded', handler);} else {var prev =
|
||||
document.onreadystatechange || function () {};document.onreadystatechange
|
||||
= function (e) {prev(e);if (document.readyState !== 'loading') {document.onreadystatechange
|
||||
= prev;handler();}};}})();</script></body>\n</html>\n"
|
||||
headers:
|
||||
Access-Control-Allow-Credentials:
|
||||
- 'true'
|
||||
Access-Control-Allow-Headers:
|
||||
- Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type,
|
||||
Access-Control-Request-Method, Access-Control-Request-Headers
|
||||
Access-Control-Allow-Methods:
|
||||
- GET,POST,OPTIONS
|
||||
Access-Control-Allow-Origin:
|
||||
- https://www.garmin.com
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-RAY:
|
||||
- 7f1ab8a0eff61559-QRO
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Language:
|
||||
- en
|
||||
Content-Type:
|
||||
- text/html;charset=UTF-8
|
||||
Date:
|
||||
- Fri, 04 Aug 2023 23:53:41 GMT
|
||||
NEL:
|
||||
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=j5pKqMzrTlGTnexO0FnuZsm0YObQFg1OH0auGBikdNQ44TMOIITdLtHmkIg36gVUZ65RQe4mMPXUL0SfZUdBcVPtg%2F3Dr3d4GgcIueMqtynkohsWR86sKXRVMZroPe%2Fp"}],"group":"cf-nel","max_age":604800}'
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
|
||||
Path=SANITIZED
|
||||
- __cf_bm=SANITIZED; path=SANITIZED; expires=SANITIZED; domain=SANITIZED; HttpOnly;
|
||||
Secure; SameSite=SANITIZED
|
||||
- __cflb=SANITIZED; SameSite=SANITIZED; Secure; path=SANITIZED; expires=SANITIZED;
|
||||
HttpOnly
|
||||
- _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Application-Context:
|
||||
- casServer:cloud,prod,prod-US_1102:6
|
||||
X-B3-Traceid:
|
||||
- 3d744417e02674885d3c4741abc1daad
|
||||
X-Robots-Tag:
|
||||
- noindex
|
||||
X-Vcap-Request-Id:
|
||||
- d62a3e74-b259-4a55-437a-2635831aa9f2
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Connection:
|
||||
- keep-alive
|
||||
Cookie:
|
||||
- __cf_bm=SANITIZED; _cfuvid=SANITIZED; __cflb=SANITIZED; org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED
|
||||
User-Agent:
|
||||
- Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15
|
||||
(KHTML, like Gecko) Mobile/15E148
|
||||
referer:
|
||||
- https://sso.garmin.com/sso/embed?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso
|
||||
method: GET
|
||||
uri: https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
|
||||
response:
|
||||
body:
|
||||
string: "<!DOCTYPE html>\n<html lang=\"en\" class=\"no-js\">\n <head>\n <meta
|
||||
http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n <meta
|
||||
name=\"viewport\" content=\"width=device-width\" />\n <meta http-equiv=\"X-UA-Compatible\"
|
||||
content=\"IE=edge;\" />\n <title>GARMIN Authentication Application</title>\n
|
||||
\ <link href=\"/sso/css/GAuth.css?20210406\" rel=\"stylesheet\" type=\"text/css\"
|
||||
media=\"all\" />\n\n\t <link rel=\"stylesheet\" href=\"\"/>\n\n <script
|
||||
type=\"text/javascript\" src=\"/sso/js/jquery/3.1.1/jquery.min.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\">jQuery.noConflict();</script>\n\t\t<script
|
||||
type=\"text/javascript\" src=\"/sso/js/jquery-validate/1.16.0/jquery.validate.min.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/jsUtils.js?20210406\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/json2.js\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/consoleUtils.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/postmessage.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/popupWindow.js\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/base.js?20210406\"></script>\n\t\t<script
|
||||
type=\"text/javascript\" src=\"/sso/js/gigyaUtils.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/login.js?20211102\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/reCaptchaUtil.js?20230706\"></script>\n\n
|
||||
\ <script>\n var recaptchaSiteKey = null;\n var
|
||||
reCaptchaURL = \"\\\\\\/sso\\\\\\/reCaptcha?id=gauth-widget\\u0026embedWidget=true\\u0026gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\";\n
|
||||
\ var isRecaptchaEnabled = null;\n var recaptchaToken
|
||||
= null; \n </script>\n <script type=\"text/javascript\">\n
|
||||
\ var parent_url = \"https:\\/\\/sso.garmin.com\\/sso\\/embed\";\n
|
||||
\ var status \t\t\t= \"\";\n\t\t\tvar result = \"\";\n\t\t\tvar
|
||||
clientId\t\t= '';\n\t\t\tvar embedWidget \t= true;\n\t\t\tvar isUsernameDefined
|
||||
= (false == true) || (false == true);\n\n // Gigya callback to
|
||||
SocialSignInController for brand new social network users redirects to this
|
||||
page\n // to popup Create or Link Social Account page, but has
|
||||
a possibly mangled source parameter\n // where \"?\" is set as
|
||||
\"<QM>\", so translate it back to \"?\" here.\n parent_url = parent_url.replace('<QM>',
|
||||
'?');\n var parent_scheme = parent_url.substring(0, parent_url.indexOf(\"://\"));\n
|
||||
\ var parent_hostname = parent_url.substring(parent_scheme.length
|
||||
+ 3, parent_url.length);\n if (parent_hostname.indexOf(\"/\") !=
|
||||
-1) {\n parent_hostname = parent_hostname.substring(0, parent_hostname.indexOf(\"/\"));\n
|
||||
\ }\n var parentHost \t = parent_scheme + \"://\"
|
||||
+ parent_hostname;\n\t\t\tvar createAccountConfigURL = '\\/sso\\/createNewAccount?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26service%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26source%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountLoginUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountCreationUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed';\n
|
||||
\ var socialConfigURL = 'https://sso.garmin.com/sso/socialSignIn?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26service%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26source%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountLoginUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountCreationUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed';\n
|
||||
\ var gigyaURL = \"https://cdns.gigya.com/js/gigya.js?apiKey=2_R3ZGY8Bqlwwk3_63knoD9wA_m-Y19mAgW61bF_s5k9gymYnMEAtMrJiF5MjF-U7B\";\n\n
|
||||
\ if (createAccountConfigURL.indexOf('%253A%252F%252F') != -1) {\n
|
||||
\ \tcreateAccountConfigURL = decodeURIComponent(createAccountConfigURL);\n
|
||||
\ }\n consoleInfo('signin.html embedWidget: true, createAccountConfigURL:
|
||||
\\/sso\\/createNewAccount?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26service%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26source%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountLoginUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountCreationUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed,
|
||||
socialEnabled: true, gigyaSupported: true, socialConfigURL(): https://sso.garmin.com/sso/socialSignIn?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26service%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26source%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountLoginUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountCreationUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed');\n\n
|
||||
\ if (socialConfigURL.indexOf('%3A%2F%2F') != -1) {\n \tsocialConfigURL
|
||||
= decodeURIComponent(socialConfigURL);\n }\n\n if( status
|
||||
!= null && status != ''){\n \tsend({'status':status});\n }\n\n
|
||||
\ jQuery(document).ready( function(){\n\n\n consoleInfo(\"signin.html:
|
||||
setting field validation rules...\");\n\n jQuery(\"#username\").rules(\"add\",{\n
|
||||
\ required: true,\n messages: {\n required:
|
||||
\ \"Email is required.\"\n }});\n\n jQuery(\"#password\").rules(\"add\",
|
||||
{\n required: true,\n messages: {\n
|
||||
\ required: \"Password is required.\"\n }\n
|
||||
\ });\n\n consoleInfo(\"signin.html: done setting
|
||||
field validation rules...\");\n\n });\n\n XD.receiveMessage(function(m){\n
|
||||
\ consoleInfo(\"signin.html: \" + m.data + \" received on \"
|
||||
+ window.location.host);\n if (m && m.data) {\n var
|
||||
md = m.data;\n if (typeof(md) === 'string') {\n md
|
||||
= JSON.parse(m.data);\n }\n if (md.setUsername)
|
||||
{\n consoleInfo(\"signin.html: Setting username \\\"\"
|
||||
+ md.username + \"\\\"...\");\n jQuery(\"#signInWithDiffLink\").click();
|
||||
// Ensure the normal login form is shown.\n jQuery(\"#username\").val(md.username);\n
|
||||
\ jQuery(\"#password\").focus();\n }\n
|
||||
\ }\n }, parentHost);\n </script>\n </head>\n
|
||||
\ <body>\n\n <!-- begin GAuth component -->\n <div id=\"GAuth-component\">\n
|
||||
\ <!-- begin login component-->\n <div id=\"login-component\"
|
||||
class=\"blueForm-basic\">\n <input type=\"hidden\" id=\"queryString\"
|
||||
value=\"id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\"
|
||||
/>\n\t \t <input type=\"hidden\" id=\"contextPath\" value=\"/sso\" />\n
|
||||
\ <!-- begin login form -->\n <div id=\"login-state-default\">\n
|
||||
\ <h2>Sign In</h2>\n\n <form method=\"post\"
|
||||
id=\"login-form\">\n\n <div class=\"form-alert\">\n\t\t\t\t\t\t\t\n
|
||||
\ \n \n \n
|
||||
\ \n \n \n\n
|
||||
\ <div id=\"username-error\" style=\"display:none;\"></div>\n
|
||||
\ <div id=\"password-error\" style=\"display:none;\"></div>\n
|
||||
\ </div>\n <div class=\"textfield\">\n\t\t\t\t\t\t\t<label
|
||||
for=\"username\">Email</label>\n \t\t<!-- If the
|
||||
lockToEmailAddress parameter is specified then we want to mark the field as
|
||||
readonly,\n \t\tpreload the email address, and disable
|
||||
the other input so that null isn't sent to the server. We'll\n \t\talso
|
||||
style the field to have a darker grey background and disable the mouse pointer\n
|
||||
\ \t\t -->\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t<!--
|
||||
If the lockToEmailAddress parameter is NOT specified then keep the existing
|
||||
functionality and disable the readonly input field\n\t\t\t\t\t\t\t -->\n\t\t\t\t\t\t\t
|
||||
\ <input class=\"login_email\" name=\"username\" id=\"username\" value=\"\"
|
||||
type=\"email\" spellcheck=\"false\" autocorrect=\"off\" autocapitalize=\"off\"/>\n\n
|
||||
\ </div>\n\n <div class=\"textfield\">\n
|
||||
\ <label for=\"password\">Password</label>\n <a
|
||||
id=\"loginforgotpassword\" class=\"login-forgot-password\" style=\"cursor:pointer\">(Forgot?)</a>\n
|
||||
\ <input type=\"password\" name=\"password\" id=\"password\"
|
||||
spellcheck=\"false\" autocorrect=\"off\" autocapitalize=\"off\" />\n <strong
|
||||
id=\"capslock-warning\" class=\"information\" title=\"Caps lock is on.\" style=\"display:
|
||||
none;\">Caps lock is on.</strong>\n\t\t\t\t\t </div>\n <input
|
||||
type=\"hidden\" name=\"embed\" value=\"true\"/>\n <input
|
||||
type=\"hidden\" name=\"_csrf\" value=\"DAA89CB8362ABB6DB2548101BE44A857AFCE1CC8C7B0825A54361D5D1FB60E47649DA070B388D04CB797C2AD4C9B9FAF9E3C\"
|
||||
/>\n <button type=\"submit\" id=\"login-btn-signin\"
|
||||
class=\"btn1\" accesskey=\"l\">Sign In</button>\n \n\n\n
|
||||
\ <!-- The existence of the \"rememberme\" parameter
|
||||
at all will remember the user! -->\n \n\n </form>\n
|
||||
\ </div>\n <!-- end login form -->\n\n <!--
|
||||
begin Create Account message -->\n\t <div id=\"login-create-account\">\n\t
|
||||
\ \n\t </div>\n\t <!-- end Create Account
|
||||
message -->\n\n\t <!-- begin Social Sign In component -->\n\t <div
|
||||
id=\"SSI-component\">\n \n\n\t\t\t\t\t\n\t </div>\n\t
|
||||
\ <!-- end Social Sign In component -->\n <div class=\"clearfix\"></div>
|
||||
<!-- Ensure that GAuth-component div's height is computed correctly. -->\n
|
||||
\ </div>\n <!-- end login component-->\n\n\t\t</div>\n\t\t<!--
|
||||
end GAuth component -->\n\n <script type=\"text/javascript\">\n jQuery(document).ready(function(){\n
|
||||
\ \tresizePageOnLoad(jQuery(\"#GAuth-component\").height());\n\n\t\t
|
||||
\ if(isUsernameDefined == true){\n\t\t // If the user's login
|
||||
just failed, redisplay the email/username specified, and focus them in the
|
||||
password field.\n\t\t jQuery(\"#password\").focus();\n\t\t }
|
||||
else if(false == true && result != \"PASSWORD_RESET_RESULT\"){\n //
|
||||
Otherwise focus them in the username field of the login dialog.\n jQuery(\"#username\").focus();\n
|
||||
\ }\n\n // Scroll to top of iframe to fix problem
|
||||
where Firefox 3.0-3.6 browsers initially show top of iframe cutoff.\n location.href=\"#\";\n\n
|
||||
\ if(!embedWidget){\n \tjQuery('.createAccountLink').click(function(){\n\t
|
||||
\ send({'openLiteBox':'createAccountLink', 'popupUrl': createAccountConfigURL,
|
||||
'popupTitle':'Create An Account', 'clientId':clientId});\n\t });\n
|
||||
\ }\n });\n </script>\n <script>(function(){var
|
||||
js = \"window['__CF$cv$params']={r:'7f1ab8a1e90a155f'};_cpo=document.createElement('script');_cpo.nonce='',_cpo.src='/cdn-cgi/challenge-platform/scripts/invisible.js',document.getElementsByTagName('head')[0].appendChild(_cpo);\";var
|
||||
_0xh = document.createElement('iframe');_0xh.height = 1;_0xh.width = 1;_0xh.style.position
|
||||
= 'absolute';_0xh.style.top = 0;_0xh.style.left = 0;_0xh.style.border = 'none';_0xh.style.visibility
|
||||
= 'hidden';document.body.appendChild(_0xh);function handler() {var _0xi =
|
||||
_0xh.contentDocument || _0xh.contentWindow.document;if (_0xi) {var _0xj =
|
||||
_0xi.createElement('script');_0xj.innerHTML = js;_0xi.getElementsByTagName('head')[0].appendChild(_0xj);}}if
|
||||
(document.readyState !== 'loading') {handler();} else if (window.addEventListener)
|
||||
{document.addEventListener('DOMContentLoaded', handler);} else {var prev =
|
||||
document.onreadystatechange || function () {};document.onreadystatechange
|
||||
= function (e) {prev(e);if (document.readyState !== 'loading') {document.onreadystatechange
|
||||
= prev;handler();}};}})();</script></body>\n</html>\n"
|
||||
headers:
|
||||
Access-Control-Allow-Credentials:
|
||||
- 'true'
|
||||
Access-Control-Allow-Headers:
|
||||
- Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type,
|
||||
Access-Control-Request-Method, Access-Control-Request-Headers
|
||||
Access-Control-Allow-Methods:
|
||||
- GET,POST,OPTIONS
|
||||
Access-Control-Allow-Origin:
|
||||
- https://www.garmin.com
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-Ray:
|
||||
- 7f1ab8a1e90a155f-QRO
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Language:
|
||||
- en
|
||||
Content-Type:
|
||||
- text/html;charset=UTF-8
|
||||
Date:
|
||||
- Fri, 04 Aug 2023 23:53:41 GMT
|
||||
NEL:
|
||||
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=2m3IsPHrodwZcDphNIdQkuKtFDRIq67h9%2BNyhtturCTJsq8UH%2BqzYY1lhYjgkKLu0YrwD8sYfVBP03Dj8Lf4R0Ghzc0o647YHYroy2Tkp2YQLDOtMwR56XKEVYEl0yhg"}],"group":"cf-nel","max_age":604800}'
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
|
||||
Path=SANITIZED
|
||||
- SESSION=SANITIZED; Path=SANITIZED; Secure; HttpOnly
|
||||
- __VCAP_ID__=SANITIZED; Path=SANITIZED; HttpOnly; Secure
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Vary:
|
||||
- Accept-Encoding
|
||||
X-Application-Context:
|
||||
- casServer:cloud,prod,prod-US_1102:5
|
||||
X-B3-Traceid:
|
||||
- 5ebef167e205ed0449ddea900e2d06fc
|
||||
X-Robots-Tag:
|
||||
- noindex
|
||||
X-Vcap-Request-Id:
|
||||
- 8bb46b1b-d486-4df3-5d3e-2767045abcdd
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: username=SANITIZED&password=SANITIZED&embed=true&_csrf=DAA89CB8362ABB6DB2548101BE44A857AFCE1CC8C7B0825A54361D5D1FB60E47649DA070B388D04CB797C2AD4C9B9FAF9E3C
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '171'
|
||||
Content-Type:
|
||||
- application/x-www-form-urlencoded
|
||||
Cookie:
|
||||
- SESSION=SANITIZED; __cf_bm=SANITIZED; _cfuvid=SANITIZED; __VCAP_ID__=SANITIZED;
|
||||
__cflb=SANITIZED; org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED
|
||||
User-Agent:
|
||||
- Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15
|
||||
(KHTML, like Gecko) Mobile/15E148
|
||||
referer:
|
||||
- https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
|
||||
method: POST
|
||||
uri: https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
|
||||
response:
|
||||
body:
|
||||
string: "<!DOCTYPE html>\n<html lang=\"en\" class=\"no-js\">\n <head>\n <meta
|
||||
http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n <meta
|
||||
name=\"viewport\" content=\"width=device-width\" />\n <meta http-equiv=\"X-UA-Compatible\"
|
||||
content=\"IE=edge;\" />\n <title>GARMIN Authentication Application</title>\n
|
||||
\ <link href=\"/sso/css/GAuth.css?20210406\" rel=\"stylesheet\" type=\"text/css\"
|
||||
media=\"all\" />\n\n\t <link rel=\"stylesheet\" href=\"\"/>\n\n <script
|
||||
type=\"text/javascript\" src=\"/sso/js/jquery/3.1.1/jquery.min.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\">jQuery.noConflict();</script>\n\t\t<script
|
||||
type=\"text/javascript\" src=\"/sso/js/jquery-validate/1.16.0/jquery.validate.min.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/jsUtils.js?20210406\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/json2.js\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/consoleUtils.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/postmessage.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/popupWindow.js\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/base.js?20210406\"></script>\n\t\t<script
|
||||
type=\"text/javascript\" src=\"/sso/js/gigyaUtils.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/login.js?20211102\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/reCaptchaUtil.js?20230706\"></script>\n\n
|
||||
\ <script>\n var recaptchaSiteKey = null;\n var
|
||||
reCaptchaURL = \"\\\\\\/sso\\\\\\/reCaptcha?id=gauth-widget\\u0026embedWidget=true\\u0026gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\";\n
|
||||
\ var isRecaptchaEnabled = null;\n var recaptchaToken
|
||||
= null; \n </script>\n <script type=\"text/javascript\">\n
|
||||
\ var parent_url = \"https:\\/\\/sso.garmin.com\\/sso\\/embed\";\n
|
||||
\ var status \t\t\t= \"FAIL\";\n\t\t\tvar result = \"error\";\n\t\t\tvar
|
||||
clientId\t\t= '';\n\t\t\tvar embedWidget \t= true;\n\t\t\tvar isUsernameDefined
|
||||
= (true == true) || (true == true);\n\n // Gigya callback to SocialSignInController
|
||||
for brand new social network users redirects to this page\n //
|
||||
to popup Create or Link Social Account page, but has a possibly mangled source
|
||||
parameter\n // where \"?\" is set as \"<QM>\", so translate it
|
||||
back to \"?\" here.\n parent_url = parent_url.replace('<QM>', '?');\n
|
||||
\ var parent_scheme = parent_url.substring(0, parent_url.indexOf(\"://\"));\n
|
||||
\ var parent_hostname = parent_url.substring(parent_scheme.length
|
||||
+ 3, parent_url.length);\n if (parent_hostname.indexOf(\"/\") !=
|
||||
-1) {\n parent_hostname = parent_hostname.substring(0, parent_hostname.indexOf(\"/\"));\n
|
||||
\ }\n var parentHost \t = parent_scheme + \"://\"
|
||||
+ parent_hostname;\n\t\t\tvar createAccountConfigURL = '\\/sso\\/createNewAccount?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26service%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26source%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountLoginUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountCreationUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed';\n
|
||||
\ var socialConfigURL = 'https://sso.garmin.com/sso/socialSignIn?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26service%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26source%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountLoginUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountCreationUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed';\n
|
||||
\ var gigyaURL = \"https://cdns.gigya.com/js/gigya.js?apiKey=2_R3ZGY8Bqlwwk3_63knoD9wA_m-Y19mAgW61bF_s5k9gymYnMEAtMrJiF5MjF-U7B\";\n\n
|
||||
\ if (createAccountConfigURL.indexOf('%253A%252F%252F') != -1) {\n
|
||||
\ \tcreateAccountConfigURL = decodeURIComponent(createAccountConfigURL);\n
|
||||
\ }\n consoleInfo('signin.html embedWidget: true, createAccountConfigURL:
|
||||
\\/sso\\/createNewAccount?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26service%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26source%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountLoginUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountCreationUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed,
|
||||
socialEnabled: true, gigyaSupported: true, socialConfigURL(): https://sso.garmin.com/sso/socialSignIn?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26service%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26source%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountLoginUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountCreationUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed');\n\n
|
||||
\ if (socialConfigURL.indexOf('%3A%2F%2F') != -1) {\n \tsocialConfigURL
|
||||
= decodeURIComponent(socialConfigURL);\n }\n\n if( status
|
||||
!= null && status != ''){\n \tsend({'status':status});\n }\n\n
|
||||
\ jQuery(document).ready( function(){\n\n\n consoleInfo(\"signin.html:
|
||||
setting field validation rules...\");\n\n jQuery(\"#username\").rules(\"add\",{\n
|
||||
\ required: true,\n messages: {\n required:
|
||||
\ \"Email is required.\"\n }});\n\n jQuery(\"#password\").rules(\"add\",
|
||||
{\n required: true,\n messages: {\n
|
||||
\ required: \"Password is required.\"\n }\n
|
||||
\ });\n\n consoleInfo(\"signin.html: done setting
|
||||
field validation rules...\");\n\n });\n\n XD.receiveMessage(function(m){\n
|
||||
\ consoleInfo(\"signin.html: \" + m.data + \" received on \"
|
||||
+ window.location.host);\n if (m && m.data) {\n var
|
||||
md = m.data;\n if (typeof(md) === 'string') {\n md
|
||||
= JSON.parse(m.data);\n }\n if (md.setUsername)
|
||||
{\n consoleInfo(\"signin.html: Setting username \\\"\"
|
||||
+ md.username + \"\\\"...\");\n jQuery(\"#signInWithDiffLink\").click();
|
||||
// Ensure the normal login form is shown.\n jQuery(\"#username\").val(md.username);\n
|
||||
\ jQuery(\"#password\").focus();\n }\n
|
||||
\ }\n }, parentHost);\n </script>\n </head>\n
|
||||
\ <body>\n\n <!-- begin GAuth component -->\n <div id=\"GAuth-component\">\n
|
||||
\ <!-- begin login component-->\n <div id=\"login-component\"
|
||||
class=\"blueForm-basic\">\n <input type=\"hidden\" id=\"queryString\"
|
||||
value=\"id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\"
|
||||
/>\n\t \t <input type=\"hidden\" id=\"contextPath\" value=\"/sso\" />\n
|
||||
\ <!-- begin login form -->\n <div id=\"login-state-default\">\n
|
||||
\ <h2>Sign In</h2>\n\n <form method=\"post\"
|
||||
id=\"login-form\">\n\n <div class=\"form-alert\">\n\t\t\t\t\t\t\t\n
|
||||
\ \n \n \n
|
||||
\ \n \n <div
|
||||
id=\"status\" class=\"error\">Invalid sign in. (Passwords are case sensitive.)</div>\n\n
|
||||
\ <div id=\"username-error\" style=\"display:none;\"></div>\n
|
||||
\ <div id=\"password-error\" style=\"display:none;\"></div>\n
|
||||
\ </div>\n <div class=\"textfield\">\n\t\t\t\t\t\t\t<label
|
||||
for=\"username\">Email</label>\n \t\t<!-- If the
|
||||
lockToEmailAddress parameter is specified then we want to mark the field as
|
||||
readonly,\n \t\tpreload the email address, and disable
|
||||
the other input so that null isn't sent to the server. We'll\n \t\talso
|
||||
style the field to have a darker grey background and disable the mouse pointer\n
|
||||
\ \t\t -->\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t<!--
|
||||
If the lockToEmailAddress parameter is NOT specified then keep the existing
|
||||
functionality and disable the readonly input field\n\t\t\t\t\t\t\t -->\n\t\t\t\t\t\t\t
|
||||
\ <input class=\"login_email\" name=\"username\" id=\"username\" value=\"user@example.com\"
|
||||
type=\"email\" spellcheck=\"false\" autocorrect=\"off\" autocapitalize=\"off\"/>\n\n
|
||||
\ </div>\n\n <div class=\"textfield\">\n
|
||||
\ <label for=\"password\">Password</label>\n <a
|
||||
id=\"loginforgotpassword\" class=\"login-forgot-password\" style=\"cursor:pointer\">(Forgot?)</a>\n
|
||||
\ <input type=\"password\" name=\"password\" id=\"password\"
|
||||
spellcheck=\"false\" autocorrect=\"off\" autocapitalize=\"off\" />\n <strong
|
||||
id=\"capslock-warning\" class=\"information\" title=\"Caps lock is on.\" style=\"display:
|
||||
none;\">Caps lock is on.</strong>\n\t\t\t\t\t </div>\n <input
|
||||
type=\"hidden\" name=\"embed\" value=\"true\"/>\n <input
|
||||
type=\"hidden\" name=\"_csrf\" value=\"25BD4FF6F23ED011DADAD97BD7125D89DF74ACC8A85485B451F997AB2E42D9216133505121272347D78B445FB19881C9968B\"
|
||||
/>\n <button type=\"submit\" id=\"login-btn-signin\"
|
||||
class=\"btn1\" accesskey=\"l\">Sign In</button>\n \n\n\n
|
||||
\ <!-- The existence of the \"rememberme\" parameter
|
||||
at all will remember the user! -->\n \n\n </form>\n
|
||||
\ </div>\n <!-- end login form -->\n\n <!--
|
||||
begin Create Account message -->\n\t <div id=\"login-create-account\">\n\t
|
||||
\ \n\t </div>\n\t <!-- end Create Account
|
||||
message -->\n\n\t <!-- begin Social Sign In component -->\n\t <div
|
||||
id=\"SSI-component\">\n \n\n\t\t\t\t\t\n\t </div>\n\t
|
||||
\ <!-- end Social Sign In component -->\n <div class=\"clearfix\"></div>
|
||||
<!-- Ensure that GAuth-component div's height is computed correctly. -->\n
|
||||
\ </div>\n <!-- end login component-->\n\n\t\t</div>\n\t\t<!--
|
||||
end GAuth component -->\n\n <script type=\"text/javascript\">\n jQuery(document).ready(function(){\n
|
||||
\ \tresizePageOnLoad(jQuery(\"#GAuth-component\").height());\n\n\t\t
|
||||
\ if(isUsernameDefined == true){\n\t\t // If the user's login
|
||||
just failed, redisplay the email/username specified, and focus them in the
|
||||
password field.\n\t\t jQuery(\"#password\").focus();\n\t\t }
|
||||
else if(false == true && result != \"PASSWORD_RESET_RESULT\"){\n //
|
||||
Otherwise focus them in the username field of the login dialog.\n jQuery(\"#username\").focus();\n
|
||||
\ }\n\n // Scroll to top of iframe to fix problem
|
||||
where Firefox 3.0-3.6 browsers initially show top of iframe cutoff.\n location.href=\"#\";\n\n
|
||||
\ if(!embedWidget){\n \tjQuery('.createAccountLink').click(function(){\n\t
|
||||
\ send({'openLiteBox':'createAccountLink', 'popupUrl': createAccountConfigURL,
|
||||
'popupTitle':'Create An Account', 'clientId':clientId});\n\t });\n
|
||||
\ }\n });\n </script>\n <script>(function(){var
|
||||
js = \"window['__CF$cv$params']={r:'7f1ab8a41e554752'};_cpo=document.createElement('script');_cpo.nonce='',_cpo.src='/cdn-cgi/challenge-platform/scripts/invisible.js',document.getElementsByTagName('head')[0].appendChild(_cpo);\";var
|
||||
_0xh = document.createElement('iframe');_0xh.height = 1;_0xh.width = 1;_0xh.style.position
|
||||
= 'absolute';_0xh.style.top = 0;_0xh.style.left = 0;_0xh.style.border = 'none';_0xh.style.visibility
|
||||
= 'hidden';document.body.appendChild(_0xh);function handler() {var _0xi =
|
||||
_0xh.contentDocument || _0xh.contentWindow.document;if (_0xi) {var _0xj =
|
||||
_0xi.createElement('script');_0xj.innerHTML = js;_0xi.getElementsByTagName('head')[0].appendChild(_0xj);}}if
|
||||
(document.readyState !== 'loading') {handler();} else if (window.addEventListener)
|
||||
{document.addEventListener('DOMContentLoaded', handler);} else {var prev =
|
||||
document.onreadystatechange || function () {};document.onreadystatechange
|
||||
= function (e) {prev(e);if (document.readyState !== 'loading') {document.onreadystatechange
|
||||
= prev;handler();}};}})();</script></body>\n</html>\n"
|
||||
headers:
|
||||
Access-Control-Allow-Credentials:
|
||||
- 'true'
|
||||
Access-Control-Allow-Headers:
|
||||
- Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type,
|
||||
Access-Control-Request-Method, Access-Control-Request-Headers
|
||||
Access-Control-Allow-Methods:
|
||||
- GET,POST,OPTIONS
|
||||
Access-Control-Allow-Origin:
|
||||
- https://www.garmin.com
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-Ray:
|
||||
- 7f1ab8a41e554752-DFW
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Language:
|
||||
- en
|
||||
Content-Type:
|
||||
- text/html;charset=UTF-8
|
||||
Date:
|
||||
- Fri, 04 Aug 2023 23:53:42 GMT
|
||||
NEL:
|
||||
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=NmXSOa2OY4MXHw09DrfMkMUFE5FijBSW8oF9uituKDizIcYfhS1rFKYV0Q3ACOQVYT6Q8Iwzj6PiIL%2BBY6E4f%2BFqsB2a20zfVAiW65WDXm6hGdPkJozBoAfyFzQZzAOc"}],"group":"cf-nel","max_age":604800}'
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
|
||||
Path=SANITIZED
|
||||
- __cfruid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Vary:
|
||||
- Accept-Encoding
|
||||
X-Application-Context:
|
||||
- casServer:cloud,prod,prod-US_1102:5
|
||||
X-B3-Traceid:
|
||||
- 139f06d066cf2a2d4b988ae89e0203d7
|
||||
X-Robots-Tag:
|
||||
- noindex
|
||||
X-Vcap-Request-Id:
|
||||
- b2849c1b-b909-403f-57b6-e13f20fc9546
|
||||
status:
|
||||
code: 401
|
||||
message: Unauthorized
|
||||
version: 1
|
||||
@@ -1,749 +0,0 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Authorization:
|
||||
- Bearer SANITIZED
|
||||
Connection:
|
||||
- keep-alive
|
||||
User-Agent:
|
||||
- GCM-iOS-5.7.2.1
|
||||
referer:
|
||||
- https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
|
||||
method: GET
|
||||
uri: https://sso.garmin.com/sso/embed?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso
|
||||
response:
|
||||
body:
|
||||
string: "<html>\n\t<head>\n\t <title>GAuth Embedded Version</title>\n\t <meta
|
||||
http-equiv=\"X-UA-Compatible\" content=\"IE=edge;\" />\n\t <style type=\"text/css\">\n\t
|
||||
\ \t#gauth-widget {border: none !important;}\n\t </style>\n\t</head>\n\t<body>\n\t\t<script
|
||||
type=\"text/javascript\" src=\"/sso/js/jquery/3.7.1/jquery.min.js?20210319\"></script>\n\n<div>\n\t<pre>\n\t<span>ERROR:
|
||||
clientId parameter must be specified!!!</span>\n\n\t<span >Usage: https://sso.garmin.com/sso/embed?clientId=<clientId>&locale=<locale>...</span>\n\n\tRequest
|
||||
parameter configuration options:\n\n\tNAME REQ VALUES
|
||||
\ DESCRIPTION\n\t------------------
|
||||
\ --- -------------------------------------------------------
|
||||
\ ---------------------------------------------------------------------------------------------------\n\tclientId
|
||||
\ Yes \"MY_GARMIN\"/\"BUY_GARMIN\"/\"FLY_GARMIN\"/ Client
|
||||
identifier for your web application\n\t \"RMA\"/\"GarminConnect\"/\"OpenCaching\"/etc\n\tlocale
|
||||
\ Yes \"en\", \"bg\", \"cs\", \"da\", \"de\", \"es\",
|
||||
\"el\", \"fr\", \"hr\", User's current locale, to display the GAuth login
|
||||
widget internationalized properly.\n\t \"in\",
|
||||
\"it\", \"iw\", \"hu\", \"ms\", \"nb\", \"nl\", \"no\", \"pl\", (All the
|
||||
currently supported locales are listed in the Values section.)\n\t \"pt\",
|
||||
\"pt_BR\", \"ru\", \"sk\", \"sl\", \"fi\", \"sv\", \"tr\",\n\t \"uk\",
|
||||
\"th\", \"ja\", \"ko\", \"zh_TW\", \"zh\", \"vi_VN\"\n\tcssUrl No
|
||||
\ Absolute URL to custom CSS file. Use custom CSS
|
||||
styling for the GAuth login widget.\n\treauth No
|
||||
\ true/false (Default value is false) Specify true if
|
||||
you want to ensure that the GAuth login widget shows up,\n\t even
|
||||
if the SSO infrastructure remembers the user and would immediately log them
|
||||
in.\n\t This
|
||||
is useful if you know a user is logged on, but want a different user to be
|
||||
allowed to logon.\n\tinitialFocus No true/false (Default
|
||||
value is true) If you don't want the GAuth login widget
|
||||
to autofocus in it's \"Email or Username\" field upon initial loading,\n\t
|
||||
\ then
|
||||
specify this option and set it to false.\n\trememberMeShown No
|
||||
\ true/false (Default value is false) Whether the \"Remember
|
||||
Me\" check box is shown in the GAuth login widget.\n\trememberMeChecked No
|
||||
\ true/false (Default value is false) Whether the \"Remember
|
||||
Me\" check box feature is checked by default.\n\tcreateAccountShown No
|
||||
\ true/false (Default value is true) Whether the \"Don't
|
||||
have an account? Create One\" link is shown in the GAuth login widget.\n\tsocialEnabled
|
||||
\ No true/false (Default value is false) If
|
||||
set to false, do not show any social sign in elements or allow social sign
|
||||
ins.\n\tlockToEmailAddress No Email address to pre-load and
|
||||
lock. If specified, the specified email address will
|
||||
be pre-loaded in the main \"Email\" field in the SSO login form,\n\t as
|
||||
well as in in the \"Email Address\" field in the \"Forgot Password?\" password
|
||||
reset form,\n\t and
|
||||
both fields will be disabled so they can't be changed.\n\t (If
|
||||
for some reason you want to force re-authentications for a known customer
|
||||
account, you can make use of this option.)\n\topenCreateAccount No
|
||||
\ true/false (Default value is false) If set to true,
|
||||
immediately display the the account creation screen.\n\tdisplayNameShown No
|
||||
\ true/false (Default value is false) If set to true,
|
||||
show the \"Display Name\" field on the account creation screen, to allow the
|
||||
user\n\t to
|
||||
set their central MyGarmin display name upon account creation.\n\tglobalOptInShown
|
||||
\ No true/false (Default value is false) Whether
|
||||
the \"Global Opt-In\" check box is shown on the create account & create social
|
||||
account screens.\n\t If
|
||||
set to true these screens will show a \"Sign Up For Email\" check box with
|
||||
accompanying text\n\t \"I
|
||||
would also like to receive email about promotions and new products.\"\n\t
|
||||
\ If
|
||||
checked, the Customer 2.0 account that is created will have it's global opt-in
|
||||
flag set to true,\n\t and
|
||||
Garmin email communications will be allowed.\n\tglobalOptInChecked No
|
||||
\ true/false (Default value is false) Whether the \"Global
|
||||
Opt-In\" check box is checked by default.\n\tconsumeServiceTicket No
|
||||
\ true/false (Default value is true) IF you don't specify
|
||||
a redirectAfterAccountLoginUrl AND you set this to false, the GAuth login
|
||||
widget\n\t will
|
||||
NOT consume the service ticket assigned and will not seamlessly log you into
|
||||
your webapp.\n\t It
|
||||
will send a SUCCESS JavaScript event with the service ticket and service url
|
||||
you can take\n\t and
|
||||
explicitly validate against the SSO infrastructure yourself.\n\t (By
|
||||
using casClient's SingleSignOnUtils.authenticateServiceTicket() utility method,\n\t
|
||||
\ or
|
||||
calling web service customerWebServices_v1.2 AccountManagementService.authenticateServiceTicket().)\n\tmobile
|
||||
\ No true/false (Default value is false) Setting
|
||||
to true will cause mobile friendly views to be shown instead of the tradition
|
||||
screens.\n\ttermsOfUseUrl No Absolute URL to your custom
|
||||
terms of use URL. If not specified, defaults to http://www.garmin.com/terms\n\tprivacyStatementUrl
|
||||
\ No Absolute URL to your custom privacy statement URL. If
|
||||
not specified, defaults to http://www.garmin.com/privacy\n\tproductSupportUrl
|
||||
\ No Absolute URL to your custom product support URL. If
|
||||
not specified, defaults to http://www.garmin.com/us/support/contact\n\tgenerateExtraServiceTicket
|
||||
\ No true/false (Default value is false) If set
|
||||
to true, generate an extra unconsumed service ticket.\n\t\t (The
|
||||
service ticket validation response will include the extra service ticket.)\n\tgenerateTwoExtraServiceTickets
|
||||
\ No true/false (Default value is false) If set to true,
|
||||
generate two extra unconsumed service tickets.\n\t\t\t\t\t\t\t\t\t \t\t\t
|
||||
\ (The service ticket validation response will include the extra service
|
||||
tickets.)\n\tgenerateNoServiceTicket No true/false (Default value
|
||||
is false) If you don't want SSO to generate a service
|
||||
ticket at all when logging in to the GAuth login widget.\n (Useful
|
||||
when allowing logins to static sites that are not SSO enabled and can't consume
|
||||
the service ticket.)\n\tconnectLegalTerms No true/false (Default
|
||||
value is false) Whether to show the connectLegalTerms
|
||||
on the create account page\n\tshowTermsOfUse No true/false
|
||||
(Default value is false) Whether to show the showTermsOfUse
|
||||
on the create account page\n\tshowPrivacyPolicy No true/false
|
||||
(Default value is false) Whether to show the showPrivacyPolicy
|
||||
on the create account page\n\tshowConnectLegalAge No true/false
|
||||
(Default value is false) Whether to show the showConnectLegalAge
|
||||
on the create account page\n\tlocationPromptShown No true/false
|
||||
(Default value is false) If set to true, ask the customer
|
||||
during account creation to verify their country of residence.\n\tshowPassword
|
||||
\ No true/false (Default value is true) If
|
||||
set to false, mobile version for createAccount and login screens would hide
|
||||
the password\n\tuseCustomHeader No true/false (Default value
|
||||
is false) If set to true, the \"Sign in\" text will be
|
||||
replaced by custom text. Contact CDS team to set the i18n text for your client
|
||||
id.\n\tmfaRequired No true/false (Default value is false)
|
||||
\ Require multi factor authentication for all authenticating
|
||||
users.\n\tperformMFACheck No true/false (Default value is
|
||||
false) If set to true, ask the logged in user to pass
|
||||
a multi factor authentication check. (Only valid for an already logged in
|
||||
user.)\n\trememberMyBrowserShown No true/false (Default value is
|
||||
false) Whether the \"Remember My Browser\" check box
|
||||
is shown in the GAuth login widget MFA verification screen.\n\trememberMyBrowserChecked
|
||||
\ No true/false (Default value is false) Whether
|
||||
the \"Remember My Browser\" check box feature is checked by default.\n\tconsentTypeIds\t\t\t\t\tNo\tconsent_types
|
||||
ids\t\t \t\t\t\t\t\t\t\t multiple consent types ids can be passed as consentTypeIds=type1&consentTypeIds=type2\n\t</pre>\n</div>\n\n\n\t<script>(function(){function
|
||||
c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML=\"window.__CF$cv$params={r:'949c83cf2bfb5e42',t:'MTc0ODkyNTY1Mi4wMDAwMDA='};var
|
||||
a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);\";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var
|
||||
a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else
|
||||
if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var
|
||||
e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>\n</html>\n"
|
||||
headers:
|
||||
Access-Control-Allow-Credentials:
|
||||
- 'true'
|
||||
Access-Control-Allow-Headers:
|
||||
- Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type,
|
||||
Access-Control-Request-Method, Access-Control-Request-Headers
|
||||
Access-Control-Allow-Methods:
|
||||
- GET,POST,OPTIONS
|
||||
Access-Control-Allow-Origin:
|
||||
- https://www.garmin.com
|
||||
CF-RAY:
|
||||
- 949c83cf2bfb5e42-QRO
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Language:
|
||||
- en
|
||||
Content-Type:
|
||||
- text/html;charset=UTF-8
|
||||
Date:
|
||||
- Tue, 03 Jun 2025 04:40:52 GMT
|
||||
NEL:
|
||||
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=cwd6V1kar7GXC7ImUBfvrwg3vgZw4sMdraKN0bkZjRt%2Bsu4gSDU%2Bv0N%2BSUhVzY7ZkTgMTuIkEmTRl7ywQ5Z%2FAD3BUh03xdXX%2B2qCgU0plnOrl93fBAlMcDC9U%2FMRzoHW"}],"group":"cf-nel","max_age":604800}'
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
|
||||
Path=SANITIZED
|
||||
- __cf_bm=SANITIZED; path=SANITIZED; expires=SANITIZED; domain=SANITIZED; HttpOnly;
|
||||
Secure; SameSite=SANITIZED
|
||||
- __cflb=SANITIZED; SameSite=SANITIZED; Secure; path=SANITIZED; expires=SANITIZED;
|
||||
HttpOnly
|
||||
- _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Application-Context:
|
||||
- casServer:cloud,prod,prod-US_Olathe:3
|
||||
X-B3-Traceid:
|
||||
- 85cea212845648ad7fbb7b5ad97acb70
|
||||
X-Robots-Tag:
|
||||
- noindex
|
||||
X-Vcap-Request-Id:
|
||||
- 85cea212-8456-48ad-7fbb-7b5ad97acb70
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Authorization:
|
||||
- Bearer SANITIZED
|
||||
Connection:
|
||||
- keep-alive
|
||||
Cookie:
|
||||
- org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
|
||||
__cflb=SANITIZED; __cf_bm=SANITIZED; _cfuvid=SANITIZED
|
||||
User-Agent:
|
||||
- GCM-iOS-5.7.2.1
|
||||
referer:
|
||||
- https://sso.garmin.com/sso/embed?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso
|
||||
method: GET
|
||||
uri: https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
|
||||
response:
|
||||
body:
|
||||
string: "<!DOCTYPE html>\n<html lang=\"en\" class=\"no-js\">\n <head>\n <meta
|
||||
http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n <meta
|
||||
name=\"viewport\" content=\"width=device-width\" />\n <meta http-equiv=\"X-UA-Compatible\"
|
||||
content=\"IE=edge;\" />\n <title>GARMIN Authentication Application</title>\n
|
||||
\ <link href=\"/sso/css/GAuth.css?20210406\" rel=\"stylesheet\" type=\"text/css\"
|
||||
media=\"all\" />\n\n\t <link rel=\"stylesheet\" href=\"\"/>\n\n <script
|
||||
type=\"text/javascript\" src=\"/sso/js/jquery/3.7.1/jquery.min.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\">jQuery.noConflict();</script>\n\t\t<script
|
||||
type=\"text/javascript\" src=\"/sso/js/jquery-validate/1.16.0/jquery.validate.min.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/jsUtils.js?20210406\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/json2.js\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/consoleUtils.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/postmessage.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/popupWindow.js\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/base.js?20231020\"></script>\n\t\t<script
|
||||
type=\"text/javascript\" src=\"/sso/js/gigyaUtils.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/login.js?20211102\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/reCaptchaUtil.js?20230706\"></script>\n\n
|
||||
\ <script>\n var recaptchaSiteKey = null;\n var
|
||||
reCaptchaURL = \"\\\\\\/sso\\\\\\/reCaptcha?id=gauth-widget\\u0026embedWidget=true\\u0026gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\\u0026redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\";\n
|
||||
\ var isRecaptchaEnabled = null;\n var recaptchaToken
|
||||
= null; \n </script>\n <script type=\"text/javascript\">\n
|
||||
\ var parent_url = \"https:\\/\\/sso.garmin.com\\/sso\\/embed\";\n
|
||||
\ var status \t\t\t= \"\";\n\t\t\tvar result = \"\";\n\t\t\tvar
|
||||
clientId\t\t= '';\n\t\t\tvar embedWidget \t= true;\n\t\t\tvar isUsernameDefined
|
||||
= (false == true) || (false == true);\n\n // Gigya callback to
|
||||
SocialSignInController for brand new social network users redirects to this
|
||||
page\n // to popup Create or Link Social Account page, but has
|
||||
a possibly mangled source parameter\n // where \"?\" is set as
|
||||
\"<QM>\", so translate it back to \"?\" here.\n parent_url = parent_url.replace('<QM>',
|
||||
'?');\n var parent_scheme = parent_url.substring(0, parent_url.indexOf(\"://\"));\n
|
||||
\ var parent_hostname = parent_url.substring(parent_scheme.length
|
||||
+ 3, parent_url.length);\n if (parent_hostname.indexOf(\"/\") !=
|
||||
-1) {\n parent_hostname = parent_hostname.substring(0, parent_hostname.indexOf(\"/\"));\n
|
||||
\ }\n var parentHost \t = parent_scheme + \"://\"
|
||||
+ parent_hostname;\n\t\t\tvar createAccountConfigURL = '\\/sso\\/createNewAccount?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26service%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26source%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountLoginUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountCreationUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed';\n
|
||||
\ var socialConfigURL = 'https://sso.garmin.com/sso/socialSignIn?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26service%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26source%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountLoginUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountCreationUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed';\n
|
||||
\ var gigyaURL = \"https://cdns.gigya.com/js/gigya.js?apiKey=2_R3ZGY8Bqlwwk3_63knoD9wA_m-Y19mAgW61bF_s5k9gymYnMEAtMrJiF5MjF-U7B\";\n\n
|
||||
\ if (createAccountConfigURL.indexOf('%253A%252F%252F') != -1) {\n
|
||||
\ \tcreateAccountConfigURL = decodeURIComponent(createAccountConfigURL);\n
|
||||
\ }\n consoleInfo('signin.html embedWidget: true, createAccountConfigURL:
|
||||
\\/sso\\/createNewAccount?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26service%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26source%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountLoginUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed%26redirectAfterAccountCreationUrl%3Dhttps%253A%252F%252Fsso.garmin.com%252Fsso%252Fembed,
|
||||
socialEnabled: true, gigyaSupported: true, socialConfigURL(): https://sso.garmin.com/sso/socialSignIn?id%3Dgauth-widget%26embedWidget%3Dtrue%26gauthHost%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26service%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26source%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountLoginUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed%26redirectAfterAccountCreationUrl%3Dhttps%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed');\n\n
|
||||
\ if (socialConfigURL.indexOf('%3A%2F%2F') != -1) {\n \tsocialConfigURL
|
||||
= decodeURIComponent(socialConfigURL);\n }\n\n if( status
|
||||
!= null && status != ''){\n \tsend({'status':status});\n }\n\n
|
||||
\ jQuery(document).ready( function(){\n\n\n consoleInfo(\"signin.html:
|
||||
setting field validation rules...\");\n\n jQuery(\"#username\").rules(\"add\",{\n
|
||||
\ required: true,\n messages: {\n required:
|
||||
\ \"Email is required.\"\n }});\n\n jQuery(\"#password\").rules(\"add\",
|
||||
{\n required: true,\n messages: {\n
|
||||
\ required: \"Password is required.\"\n }\n
|
||||
\ });\n\n consoleInfo(\"signin.html: done setting
|
||||
field validation rules...\");\n\n });\n\n XD.receiveMessage(function(m){\n
|
||||
\ consoleInfo(\"signin.html: \" + m.data + \" received on \"
|
||||
+ window.location.host);\n if (m && m.data) {\n var
|
||||
md = m.data;\n if (typeof(md) === 'string') {\n md
|
||||
= JSON.parse(m.data);\n }\n if (md.setUsername)
|
||||
{\n consoleInfo(\"signin.html: Setting username \\\"\"
|
||||
+ md.username + \"\\\"...\");\n jQuery(\"#signInWithDiffLink\").click();
|
||||
// Ensure the normal login form is shown.\n jQuery(\"#username\").val(md.username);\n
|
||||
\ jQuery(\"#password\").focus();\n }\n
|
||||
\ }\n }, parentHost);\n </script>\n </head>\n
|
||||
\ <body>\n\n <!-- begin GAuth component -->\n <div id=\"GAuth-component\">\n
|
||||
\ <!-- begin login component-->\n <div id=\"login-component\"
|
||||
class=\"blueForm-basic\">\n <input type=\"hidden\" id=\"queryString\"
|
||||
value=\"id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\"
|
||||
/>\n\t \t <input type=\"hidden\" id=\"contextPath\" value=\"/sso\" />\n
|
||||
\ <!-- begin login form -->\n <div id=\"login-state-default\">\n
|
||||
\ <h2>Sign In</h2>\n\n <form method=\"post\"
|
||||
id=\"login-form\">\n\n <div class=\"form-alert\">\n\t\t\t\t\t\t\t\n
|
||||
\ \n \n \n
|
||||
\ \n \n \n\n
|
||||
\ <div id=\"username-error\" style=\"display:none;\"></div>\n
|
||||
\ <div id=\"password-error\" style=\"display:none;\"></div>\n
|
||||
\ </div>\n <div class=\"textfield\">\n\t\t\t\t\t\t\t<label
|
||||
for=\"username\">Email</label>\n \t\t<!-- If the
|
||||
lockToEmailAddress parameter is specified then we want to mark the field as
|
||||
readonly,\n \t\tpreload the email address, and disable
|
||||
the other input so that null isn't sent to the server. We'll\n \t\talso
|
||||
style the field to have a darker grey background and disable the mouse pointer\n
|
||||
\ \t\t -->\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t<!--
|
||||
If the lockToEmailAddress parameter is NOT specified then keep the existing
|
||||
functionality and disable the readonly input field\n\t\t\t\t\t\t\t -->\n\t\t\t\t\t\t\t
|
||||
\ <input class=\"login_email\" name=\"username\" id=\"username\" value=\"\"
|
||||
type=\"email\" spellcheck=\"false\" autocorrect=\"off\" autocapitalize=\"off\"/>\n\n
|
||||
\ </div>\n\n <div class=\"textfield\">\n
|
||||
\ <label for=\"password\">Password</label>\n <a
|
||||
id=\"loginforgotpassword\" class=\"login-forgot-password\" style=\"cursor:pointer\">(Forgot?)</a>\n
|
||||
\ <input type=\"password\" name=\"password\" id=\"password\"
|
||||
spellcheck=\"false\" autocorrect=\"off\" autocapitalize=\"off\" />\n <strong
|
||||
id=\"capslock-warning\" class=\"information\" title=\"Caps lock is on.\" style=\"display:
|
||||
none;\">Caps lock is on.</strong>\n\t\t\t\t\t </div>\n <input
|
||||
type=\"hidden\" name=\"embed\" value=\"true\"/>\n <input
|
||||
type=\"hidden\" name=\"_csrf\" value=\"90280BE13709DE2C0CF38CAB2A77E3FC82F62894F2396D07630AD246706B197735797D02C4592A6D5AB3B8BF1F3B80460522\"
|
||||
/>\n <button type=\"submit\" id=\"login-btn-signin\"
|
||||
class=\"btn1\" accesskey=\"l\">Sign In</button>\n \n\n\n
|
||||
\ <!-- The existence of the \"rememberme\" parameter
|
||||
at all will remember the user! -->\n \n\n </form>\n
|
||||
\ </div>\n <!-- end login form -->\n\n <!--
|
||||
begin Create Account message -->\n\t <div id=\"login-create-account\">\n\t
|
||||
\ \n\t </div>\n\t <!-- end Create Account
|
||||
message -->\n\n\t <!-- begin Social Sign In component -->\n\t <div
|
||||
id=\"SSI-component\">\n \n\n\t\t\t\t\t\n\t </div>\n\t
|
||||
\ <!-- end Social Sign In component -->\n <div class=\"clearfix\"></div>
|
||||
<!-- Ensure that GAuth-component div's height is computed correctly. -->\n
|
||||
\ </div>\n <!-- end login component-->\n\n\t\t</div>\n\t\t<!--
|
||||
end GAuth component -->\n\n <script type=\"text/javascript\">\n jQuery(document).ready(function(){\n
|
||||
\ \tresizePageOnLoad(jQuery(\"#GAuth-component\").height());\n\n\t\t
|
||||
\ if(isUsernameDefined == true){\n\t\t // If the user's login
|
||||
just failed, redisplay the email/username specified, and focus them in the
|
||||
password field.\n\t\t jQuery(\"#password\").focus();\n\t\t }
|
||||
else if(false == true && result != \"PASSWORD_RESET_RESULT\"){\n //
|
||||
Otherwise focus them in the username field of the login dialog.\n jQuery(\"#username\").focus();\n
|
||||
\ }\n\n // Scroll to top of iframe to fix problem
|
||||
where Firefox 3.0-3.6 browsers initially show top of iframe cutoff.\n location.href=\"#\";\n\n
|
||||
\ if(!embedWidget){\n \tjQuery('.createAccountLink').click(function(){\n\t
|
||||
\ send({'openLiteBox':'createAccountLink', 'popupUrl': createAccountConfigURL,
|
||||
'popupTitle':'Create An Account', 'clientId':clientId});\n\t });\n
|
||||
\ }\n });\n </script>\n <script>(function(){function
|
||||
c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML=\"window.__CF$cv$params={r:'949c83d17d414f14',t:'MTc0ODkyNTY1Mi4wMDAwMDA='};var
|
||||
a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);\";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var
|
||||
a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else
|
||||
if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var
|
||||
e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>\n</html>\n"
|
||||
headers:
|
||||
Access-Control-Allow-Credentials:
|
||||
- 'true'
|
||||
Access-Control-Allow-Headers:
|
||||
- Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type,
|
||||
Access-Control-Request-Method, Access-Control-Request-Headers
|
||||
Access-Control-Allow-Methods:
|
||||
- GET,POST,OPTIONS
|
||||
Access-Control-Allow-Origin:
|
||||
- https://www.garmin.com
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-Ray:
|
||||
- 949c83d17d414f14-QRO
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Language:
|
||||
- en
|
||||
Content-Type:
|
||||
- text/html;charset=UTF-8
|
||||
Date:
|
||||
- Tue, 03 Jun 2025 04:40:52 GMT
|
||||
NEL:
|
||||
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=REffANT2%2FfOZY9xYp%2FXinxsCOsc73u6TBWc0qVetJyK9oQhy63N6Qk3fNr5TDiEV9JM9RIKw5uZhoVeBr7vDZK1f0UsNdTsjHdr19V0Lnt%2FCqbU6Y3MTWpcTaQYMqUIo"}],"group":"cf-nel","max_age":604800}'
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
|
||||
Path=SANITIZED
|
||||
- SESSION=SANITIZED; Path=SANITIZED; Secure; HttpOnly
|
||||
- __VCAP_ID__=SANITIZED; Path=SANITIZED; HttpOnly; Secure
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Vary:
|
||||
- Accept-Encoding
|
||||
X-Application-Context:
|
||||
- casServer:cloud,prod,prod-US_Olathe:6
|
||||
X-B3-Traceid:
|
||||
- 77e60c0ac1d641c074820aac41fbde80
|
||||
X-Robots-Tag:
|
||||
- noindex
|
||||
X-Vcap-Request-Id:
|
||||
- 77e60c0a-c1d6-41c0-7482-0aac41fbde80
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: username=SANITIZED&password=SANITIZED&embed=true&_csrf=90280BE13709DE2C0CF38CAB2A77E3FC82F62894F2396D07630AD246706B197735797D02C4592A6D5AB3B8BF1F3B80460522
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Authorization:
|
||||
- Bearer SANITIZED
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '177'
|
||||
Content-Type:
|
||||
- application/x-www-form-urlencoded
|
||||
Cookie:
|
||||
- SESSION=SANITIZED; org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
|
||||
__cflb=SANITIZED; __VCAP_ID__=SANITIZED; __cf_bm=SANITIZED; _cfuvid=SANITIZED
|
||||
User-Agent:
|
||||
- GCM-iOS-5.7.2.1
|
||||
referer:
|
||||
- https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
|
||||
method: POST
|
||||
uri: https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
|
||||
response:
|
||||
body:
|
||||
string: ''
|
||||
headers:
|
||||
Access-Control-Allow-Credentials:
|
||||
- 'true'
|
||||
Access-Control-Allow-Headers:
|
||||
- Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type,
|
||||
Access-Control-Request-Method, Access-Control-Request-Headers
|
||||
Access-Control-Allow-Methods:
|
||||
- GET,POST,OPTIONS
|
||||
Access-Control-Allow-Origin:
|
||||
- https://www.garmin.com
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-Ray:
|
||||
- 949c83d32cf657bd-QRO
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Language:
|
||||
- en
|
||||
Content-Length:
|
||||
- '0'
|
||||
Date:
|
||||
- Tue, 03 Jun 2025 04:40:54 GMT
|
||||
Location:
|
||||
- https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
|
||||
NEL:
|
||||
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=8rQsZTg41dTwDgWNQriYHPcbY3UG6NQ0v%2FN6zaizxXzpDFLJALfe7s%2BopIWHB0dvU9WeEEUreQPI2Wlkgz2Gp6z9fx51UvQZtS3N2hIGKyEW7QNno8eCyuMyHYGXjJOx"}],"group":"cf-nel","max_age":604800}'
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
|
||||
Path=SANITIZED
|
||||
- __cfruid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED
|
||||
Vary:
|
||||
- Accept-Encoding
|
||||
X-Application-Context:
|
||||
- casServer:cloud,prod,prod-US_Olathe:6
|
||||
X-B3-Traceid:
|
||||
- 1da874cc48894fdf4e1ac9d9e8e269c8
|
||||
X-Robots-Tag:
|
||||
- noindex
|
||||
X-Vcap-Request-Id:
|
||||
- 1da874cc-4889-4fdf-4e1a-c9d9e8e269c8
|
||||
status:
|
||||
code: 302
|
||||
message: Found
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Authorization:
|
||||
- Bearer SANITIZED
|
||||
Connection:
|
||||
- keep-alive
|
||||
Cookie:
|
||||
- SESSION=SANITIZED; org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
|
||||
__cflb=SANITIZED; __VCAP_ID__=SANITIZED; __cf_bm=SANITIZED; _cfuvid=SANITIZED;
|
||||
__cfruid=SANITIZED
|
||||
User-Agent:
|
||||
- GCM-iOS-5.7.2.1
|
||||
referer:
|
||||
- https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
|
||||
method: GET
|
||||
uri: https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
|
||||
response:
|
||||
body:
|
||||
string: "<!DOCTYPE html>\n<html lang=\"en\" class=\"no-js\">\n\n<head>\n <script
|
||||
type=\"text/javascript\" src=\"/sso/js/jquery/3.7.1/jquery.min.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\">jQuery.noConflict();</script>\n <script
|
||||
type=\"text/javascript\" src=\"/sso/js/jquery-validate/1.16.0/jquery.validate.min.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/base.js?20231020\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/jsUtils.js?20210406\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/json2.js\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/postmessage.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/consoleUtils.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/setupMfaRequiredView.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/enterMfaCode.js?20230127\"></script>\n
|
||||
\ <script type=\"text/javascript\">\n var embedWidget = \"true\";\n
|
||||
\ if (embedWidget == \"\") {\n embedWidget = \"\";\n }\n
|
||||
\ embedWidget = (embedWidget == \"true\");\n var parent_url =
|
||||
\"https:\\/\\/sso.garmin.com\\/sso\\/embed\";\n window.onload = function()
|
||||
{\n ifrememberMyBrowserChecked();\n };\n\n jQuery(document).ready(
|
||||
function() {\n if (!embedWidget) {\n send({'gauthHeight':
|
||||
jQuery(\"#GAuth-component\").height()});\n }\n jQuery(\"#mfa-verification-code-submit\").click(function(){\n
|
||||
\ if (!validateMfaCodeAndPrivacyConsents()){\n return
|
||||
false;\n }\n jQuery('#submit-mfa-verification-code-form').submit();\n
|
||||
\ return false;\n });\n });\n var customerGuid
|
||||
= \"0690cc1d-d23d-4412-b027-80fd4ed1c0f6\";\n var mfaMethod = \"email\";\n
|
||||
\ var locale = \"\";\n var clientId = \"\";\n var codeSentTo
|
||||
= \"mt*****@gmail.com\";\n </script>\n <meta charset=\"utf-8\">\n <title>Enter
|
||||
MFA code for login</title>\n <meta name=\"description\" content=\"\">\n
|
||||
\ <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n
|
||||
\ <meta http-equiv=\"cleartype\" content=\"on\">\n <meta http-equiv=\"X-UA-Compatible\"
|
||||
content=\"IE=edge;\" />\n <link href=\"/sso/css/GAuth.css?20170505\" rel=\"stylesheet\"
|
||||
type=\"text/css\" media=\"all\" />\n <link rel=\"stylesheet\" href=\"\"
|
||||
/>\n</head>\n\n<body>\n <div id=\"GAuth-component\">\n <h2 id=\"enter-mfa-code-h2\">Enter
|
||||
security code</h2>\n <input type=\"hidden\" id=\"queryString\" value=\"id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\"
|
||||
/>\n <input type=\"hidden\" id=\"contextPath\" value=\"/sso\" />\n\n
|
||||
\ <div id=\"login-component\" class=\"blueForm-basic\">\n <div
|
||||
id=\"login-state-verifymfa\">\n <td>\n \n
|
||||
\ <span >Code sent to <b>mt*****@gmail.com</b></span>\n
|
||||
\ </td>\n <form id=\"submit-mfa-verification-code-form\"
|
||||
name=\"submit-mfa-verification-code-form\" method=\"post\" novalidate=\"novalidate\">\n
|
||||
\ <div class=\"blueForm-v2\">\n <div
|
||||
class=\"form-alert\">\n <div id=\"genericError\"
|
||||
class=\"error\" hidden>An unexpected error has occurred.</div>\n <div
|
||||
id=\"codeSentAttention\" class=\"attention\" hidden>A new code has been sent.
|
||||
You can request another code in 30 seconds.</div>\n \n
|
||||
\ \n <div id=\"maxLimit\"
|
||||
class=\"error\" hidden=\"hidden\">You have reached the maximum amount of codes
|
||||
requested. Please use a code you've received or wait 24 hours and try
|
||||
again.</div>\n \n </div>\n
|
||||
\ <div class=\"formTextField\">\n <div
|
||||
class=\"mfaFormLabel\">\n <label>\n <span>Security
|
||||
code</span>\n <br/>\n <input
|
||||
type=\"number\" pattern=\"[0-9]*\" inputmode=\"numeric\" maxlength=\"6\" id=\"mfa-code\"
|
||||
name=\"mfa-code\" autofocus oninput=\"validateMfaCodeAndPrivacyConsents()\"/>\n
|
||||
\ </label>\n </div>\n
|
||||
\ </div>\n <br><br>\n <div>\n
|
||||
\ <a href=\"https://support.garmin.com/en-US/?faq=uGHS8ZqOIhA0usBzBMdJu7\"
|
||||
target=\"_blank\" id=\"havingTrouble\">Get help</a><br>\n </div>\n
|
||||
\ <div id=\"requestNewCodeWrapper\" class=\"requestNewCode\">\n
|
||||
\ <a href=\"#\" id=\"newCode\">Request a new code</a>\n
|
||||
\ </div>\n \n \n
|
||||
\ <br>\n \n <br/>\n
|
||||
\ <button type=\"submit\" id=\"mfa-verification-code-submit\"
|
||||
class=\"btn1\">Next</button>\n </div>\n <input
|
||||
type=\"hidden\" name=\"embed\" value=\"true\"/>\n <input
|
||||
type=\"hidden\" name=\"_csrf\" value=\"9AF199177EE70FB2511C2DE25FE2780DEF8327EDCA5AB81C391FAF6E419E83EDF20E1EE31B76E282D8AA46124E3DC5EB1391\"
|
||||
/>\n <input type=\"hidden\" name=\"fromPage\" value=\"setupEnterMfaCode\"/>\n
|
||||
\ <br/>\n </form>\n </div>\n <div
|
||||
class=\"clearfix\"></div> <!-- Ensure that GAuth-component div's height is
|
||||
computed correctly. -->\n </div>\n </div>\n <script type=\"text/javascript\">\n
|
||||
\ resizePageOnLoad(jQuery(\"#GAuth-component\").height());\n </script>\n<script>(function(){function
|
||||
c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML=\"window.__CF$cv$params={r:'949c83da2a5ac1ca',t:'MTc0ODkyNTY1NC4wMDAwMDA='};var
|
||||
a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);\";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var
|
||||
a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else
|
||||
if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var
|
||||
e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>\n</html>"
|
||||
headers:
|
||||
Access-Control-Allow-Credentials:
|
||||
- 'true'
|
||||
Access-Control-Allow-Headers:
|
||||
- Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type,
|
||||
Access-Control-Request-Method, Access-Control-Request-Headers
|
||||
Access-Control-Allow-Methods:
|
||||
- GET,POST,OPTIONS
|
||||
Access-Control-Allow-Origin:
|
||||
- https://www.garmin.com
|
||||
CF-RAY:
|
||||
- 949c83da2a5ac1ca-QRO
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Language:
|
||||
- en
|
||||
Content-Type:
|
||||
- text/html;charset=UTF-8
|
||||
Date:
|
||||
- Tue, 03 Jun 2025 04:40:54 GMT
|
||||
NEL:
|
||||
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=BrVkK9BHKPEC700UIxYqeYMAufPXrsMtXb56Z5naqivj9pfj%2FKyqvweC0oLp4v4n%2BecNLLGdP4o5WUnke2Iu62u0i0gzh9hqR49I8mYeEw6ABEfR8ZFJbx0waSuNNous"}],"group":"cf-nel","max_age":604800}'
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
|
||||
Path=SANITIZED
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Application-Context:
|
||||
- casServer:cloud,prod,prod-US_Olathe:6
|
||||
X-B3-Traceid:
|
||||
- acd069da786e436a7d98ba4e5220bcfc
|
||||
X-Robots-Tag:
|
||||
- noindex
|
||||
X-Vcap-Request-Id:
|
||||
- acd069da-786e-436a-7d98-ba4e5220bcfc
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: mfa-code=123456&embed=true&_csrf=9AF199177EE70FB2511C2DE25FE2780DEF8327EDCA5AB81C391FAF6E419E83EDF20E1EE31B76E282D8AA46124E3DC5EB1391&fromPage=setupEnterMfaCode
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Authorization:
|
||||
- Bearer SANITIZED
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '160'
|
||||
Content-Type:
|
||||
- application/x-www-form-urlencoded
|
||||
Cookie:
|
||||
- SESSION=SANITIZED; org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
|
||||
__cflb=SANITIZED; __VCAP_ID__=SANITIZED; __cf_bm=SANITIZED; _cfuvid=SANITIZED;
|
||||
__cfruid=SANITIZED
|
||||
User-Agent:
|
||||
- GCM-iOS-5.7.2.1
|
||||
referer:
|
||||
- https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
|
||||
method: POST
|
||||
uri: https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed
|
||||
response:
|
||||
body:
|
||||
string: "<!DOCTYPE html>\n<html lang=\"en\" class=\"no-js\">\n\n<head>\n <script
|
||||
type=\"text/javascript\" src=\"/sso/js/jquery/3.7.1/jquery.min.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\">jQuery.noConflict();</script>\n <script
|
||||
type=\"text/javascript\" src=\"/sso/js/jquery-validate/1.16.0/jquery.validate.min.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/base.js?20231020\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/jsUtils.js?20210406\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/json2.js\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/postmessage.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/consoleUtils.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/setupMfaRequiredView.js?20210319\"></script>\n
|
||||
\ <script type=\"text/javascript\" src=\"/sso/js/enterMfaCode.js?20230127\"></script>\n
|
||||
\ <script type=\"text/javascript\">\n var embedWidget = \"true\";\n
|
||||
\ if (embedWidget == \"\") {\n embedWidget = \"\";\n }\n
|
||||
\ embedWidget = (embedWidget == \"true\");\n var parent_url =
|
||||
\"https:\\/\\/sso.garmin.com\\/sso\\/embed\";\n window.onload = function()
|
||||
{\n ifrememberMyBrowserChecked();\n };\n\n jQuery(document).ready(
|
||||
function() {\n if (!embedWidget) {\n send({'gauthHeight':
|
||||
jQuery(\"#GAuth-component\").height()});\n }\n jQuery(\"#mfa-verification-code-submit\").click(function(){\n
|
||||
\ if (!validateMfaCodeAndPrivacyConsents()){\n return
|
||||
false;\n }\n jQuery('#submit-mfa-verification-code-form').submit();\n
|
||||
\ return false;\n });\n });\n var customerGuid
|
||||
= \"0690cc1d-d23d-4412-b027-80fd4ed1c0f6\";\n var mfaMethod = \"email\";\n
|
||||
\ var locale = \"\";\n var clientId = \"\";\n var codeSentTo
|
||||
= \"mt*****@gmail.com\";\n </script>\n <meta charset=\"utf-8\">\n <title>Enter
|
||||
MFA code for login</title>\n <meta name=\"description\" content=\"\">\n
|
||||
\ <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n
|
||||
\ <meta http-equiv=\"cleartype\" content=\"on\">\n <meta http-equiv=\"X-UA-Compatible\"
|
||||
content=\"IE=edge;\" />\n <link href=\"/sso/css/GAuth.css?20170505\" rel=\"stylesheet\"
|
||||
type=\"text/css\" media=\"all\" />\n <link rel=\"stylesheet\" href=\"\"
|
||||
/>\n</head>\n\n<body>\n <div id=\"GAuth-component\">\n <h2 id=\"enter-mfa-code-h2\">Enter
|
||||
security code</h2>\n <input type=\"hidden\" id=\"queryString\" value=\"id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed\"
|
||||
/>\n <input type=\"hidden\" id=\"contextPath\" value=\"/sso\" />\n\n
|
||||
\ <div id=\"login-component\" class=\"blueForm-basic\">\n <div
|
||||
id=\"login-state-verifymfa\">\n <td>\n \n
|
||||
\ <span >Code sent to <b>mt*****@gmail.com</b></span>\n
|
||||
\ </td>\n <form id=\"submit-mfa-verification-code-form\"
|
||||
name=\"submit-mfa-verification-code-form\" method=\"post\" novalidate=\"novalidate\">\n
|
||||
\ <div class=\"blueForm-v2\">\n <div
|
||||
class=\"form-alert\">\n <div id=\"genericError\"
|
||||
class=\"error\" hidden>An unexpected error has occurred.</div>\n <div
|
||||
id=\"codeSentAttention\" class=\"attention\" hidden>A new code has been sent.
|
||||
You can request another code in 30 seconds.</div>\n <div
|
||||
id=\"invalidCode\" class=\"error\">Invalid code. Please enter a valid code.</div>\n
|
||||
\ \n <div id=\"maxLimit\"
|
||||
class=\"error\" hidden=\"hidden\">You have reached the maximum amount of codes
|
||||
requested. Please use a code you've received or wait 24 hours and try
|
||||
again.</div>\n \n </div>\n
|
||||
\ <div class=\"formTextField\">\n <div
|
||||
class=\"mfaFormLabel\">\n <label>\n <span>Security
|
||||
code</span>\n <br/>\n <input
|
||||
type=\"number\" pattern=\"[0-9]*\" inputmode=\"numeric\" maxlength=\"6\" id=\"mfa-code\"
|
||||
name=\"mfa-code\" autofocus oninput=\"validateMfaCodeAndPrivacyConsents()\"/>\n
|
||||
\ </label>\n </div>\n
|
||||
\ </div>\n <br><br>\n <div>\n
|
||||
\ <a href=\"https://support.garmin.com/en-US/?faq=uGHS8ZqOIhA0usBzBMdJu7\"
|
||||
target=\"_blank\" id=\"havingTrouble\">Get help</a><br>\n </div>\n
|
||||
\ <div id=\"requestNewCodeWrapper\" class=\"requestNewCode\">\n
|
||||
\ <a href=\"#\" id=\"newCode\">Request a new code</a>\n
|
||||
\ </div>\n \n \n
|
||||
\ <br>\n \n <br/>\n
|
||||
\ <button type=\"submit\" id=\"mfa-verification-code-submit\"
|
||||
class=\"btn1\">Next</button>\n </div>\n <input
|
||||
type=\"hidden\" name=\"embed\" value=\"true\"/>\n <input
|
||||
type=\"hidden\" name=\"_csrf\" value=\"\" />\n <input
|
||||
type=\"hidden\" name=\"fromPage\" value=\"setupEnterMfaCode\"/>\n
|
||||
\ <br/>\n </form>\n </div>\n <div
|
||||
class=\"clearfix\"></div> <!-- Ensure that GAuth-component div's height is
|
||||
computed correctly. -->\n </div>\n </div>\n <script type=\"text/javascript\">\n
|
||||
\ resizePageOnLoad(jQuery(\"#GAuth-component\").height());\n </script>\n<script>(function(){function
|
||||
c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML=\"window.__CF$cv$params={r:'949c83dc9aa555c3',t:'MTc0ODkyNTY1NC4wMDAwMDA='};var
|
||||
a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);\";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var
|
||||
a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else
|
||||
if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var
|
||||
e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>\n</html>"
|
||||
headers:
|
||||
Access-Control-Allow-Credentials:
|
||||
- 'true'
|
||||
Access-Control-Allow-Headers:
|
||||
- Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type,
|
||||
Access-Control-Request-Method, Access-Control-Request-Headers
|
||||
Access-Control-Allow-Methods:
|
||||
- GET,POST,OPTIONS
|
||||
Access-Control-Allow-Origin:
|
||||
- https://www.garmin.com
|
||||
CF-RAY:
|
||||
- 949c83dc9aa555c3-QRO
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Language:
|
||||
- en
|
||||
Content-Type:
|
||||
- text/html;charset=UTF-8
|
||||
Date:
|
||||
- Tue, 03 Jun 2025 04:40:54 GMT
|
||||
NEL:
|
||||
- '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}'
|
||||
Report-To:
|
||||
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=WVjXMWAY7m%2FICrofTUaszDZoZ1kIv1%2BQTcx49UDCpdhESBjLNt9LucYPatIj%2BHOhRkqNPuM%2F65Tz1kTrR4naiCX0yEAOcMcEAh1yxyiX%2BlU7qvovsvWodipj8YHB19mH"}],"group":"cf-nel","max_age":604800}'
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED;
|
||||
Path=SANITIZED
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Application-Context:
|
||||
- casServer:cloud,prod,prod-US_Olathe:6
|
||||
X-B3-Traceid:
|
||||
- 104ac62c483244cf73fb9266e97d22bb
|
||||
X-Robots-Tag:
|
||||
- noindex
|
||||
X-Vcap-Request-Id:
|
||||
- 104ac62c-4832-44cf-73fb-9266e97d22bb
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user