0.1.5 b849f6c

device_flow.go RawHistoryFolder

1package auth
2
3import (
4	"encoding/json"
5	"fmt"
6	"io"
7	"net/http"
8	"net/url"
9	"time"
10
11	"github.com/bnema/gordon/internal/common"
12)
13
14func DeviceFlowAuth(config *common.Config) error {
15	fmt.Println("Starting device flow authentication...")
16
17	// Request device code
18	deviceCode, err := requestDeviceCode(config)
19	if err != nil {
20		return fmt.Errorf("error requesting device code: %w", err)
21	}
22
23	fmt.Printf("Please go to %v and enter the code: %v\n", deviceCode["verification_uri"], deviceCode["user_code"])
24
25	// Poll for access token
26	interval := 5 // Default to 5 seconds if not provided
27	if i, ok := deviceCode["interval"].(float64); ok {
28		interval = int(i)
29	}
30
31	token, err := pollForAccessToken(config, deviceCode["device_code"].(string), interval)
32	if err != nil {
33		return fmt.Errorf("error polling for access token: %w", err)
34	}
35
36	// Save the token
37	config.SetToken(token)
38
39	fmt.Println("Authentication successful!")
40	return nil
41}
42
43func requestDeviceCode(config *common.Config) (map[string]interface{}, error) {
44	resp, err := http.PostForm(config.GetBackendURL()+"/api/device/code", url.Values{})
45	if err != nil {
46		return nil, fmt.Errorf("error making request: %w", err)
47	}
48	defer resp.Body.Close()
49
50	body, err := io.ReadAll(resp.Body)
51	if err != nil {
52		return nil, fmt.Errorf("error reading response body: %w", err)
53	}
54
55	if resp.StatusCode != http.StatusOK {
56		return nil, fmt.Errorf("server returned non-200 status code: %d. Body: %s", resp.StatusCode, string(body))
57	}
58
59	var deviceCode map[string]interface{}
60	if err := json.Unmarshal(body, &deviceCode); err != nil {
61		return nil, fmt.Errorf("error parsing JSON response: %w", err)
62	}
63
64	// Check for required fields
65	requiredFields := []string{"verification_uri", "user_code", "device_code", "interval"}
66	for _, field := range requiredFields {
67		if _, ok := deviceCode[field]; !ok {
68			return nil, fmt.Errorf("missing required field in response: %s", field)
69		}
70	}
71
72	return deviceCode, nil
73}
74
75func pollForAccessToken(config *common.Config, deviceCode string, interval int) (string, error) {
76	maxAttempts := 60 // Maximum number of attempts (5 minutes with 5-second intervals)
77	for attempt := 0; attempt < maxAttempts; attempt++ {
78		time.Sleep(time.Duration(interval) * time.Second)
79
80		resp, err := http.PostForm(config.GetBackendURL()+"/api/device/token", url.Values{
81			"device_code": {deviceCode},
82		})
83		if err != nil {
84			continue
85		}
86		defer resp.Body.Close()
87
88		body, err := io.ReadAll(resp.Body)
89		if err != nil {
90			continue
91		}
92
93		var tokenResp map[string]interface{}
94		if err := json.Unmarshal(body, &tokenResp); err != nil {
95			continue
96		}
97
98		if accessToken, ok := tokenResp["access_token"].(string); ok && accessToken != "" {
99			return accessToken, nil
100		}
101
102		if tokenResp["error"] == "authorization_pending" {
103			// Optionally, you can print a dot to show progress without cluttering the output
104			fmt.Print(".")
105			continue
106		}
107
108		// If we get here, there's an unexpected response but we can continue to try
109		// fmt.Printf("Unexpected response from server (attempt %d): %v\n", attempt+1, tokenResp)
110	}
111
112	return "", fmt.Errorf("max attempts reached, failed to obtain access token")
113}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close