Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
bin/
logs/app.log
129 changes: 84 additions & 45 deletions cli/cmd/auth_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,62 @@ package cmd

import (
"bufio"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
)

// TODO: Update with real auth URL
const authUrl = "https://example.com/login"
const (
serverBaseUrl = "http://localhost:3000"
authUrl = serverBaseUrl + "/oauth/authorize"
tokenUrl = serverBaseUrl + "/oauth/token"
clientID = "cli"
redirectURI = serverBaseUrl + "/oauth/callback"
)

type LoginCmd struct {
}

func openUrl() error {
func generateRandomString(n int) (string, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}

func generateCodeChallenge(verifier string) string {
s := sha256.New()
s.Write([]byte(verifier))
return base64.RawURLEncoding.EncodeToString(s.Sum(nil))
}

func openUrl(url string) error {
var cmd *exec.Cmd
url := authUrl
switch runtime.GOOS {
case "darwin":
cmd = exec.Command("open", url)
case "windows":
cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
default: // Linux, BSD, etc.
default: // Linux
cmd = exec.Command("xdg-open", url)
}

return cmd.Start()
}

func saveTokenToConfig(ctx *AppContext, token string) error {
func saveTokenToConfig(ctx *AppContext, token string, refreshToken string) error {
configPath := defaultConfigPath()
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
Expand All @@ -51,59 +75,74 @@ func saveTokenToConfig(ctx *AppContext, token string) error {
if err := os.WriteFile(configPath, data, 0o600); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
fmt.Println("Token saved:", token)
fmt.Println("Token saved")
return nil
}

func getLongLivedToken(shortLivedToken string) (string, error) {
// Placeholder for actual implementation to exchange short-lived token for long-lived token
// In a real scenario, this would involve making an HTTP request to the auth server
return shortLivedToken + "_long_lived", nil
}

func (l *LoginCmd) Run(ctx *AppContext) error {
// mist auth login
if ctx.Config != nil && ctx.Config.AccessToken != "" {

// Already logged in, ask if they want to re-login
fmt.Println("Already logged in with token:", ctx.Config.AccessToken)
fmt.Print("Re-enter token? (y/N): ")
reader := bufio.NewReader(os.Stdin)
answer, _ := reader.ReadString('\n')
answer = strings.TrimSpace(strings.ToLower(answer))
if answer != "y" && answer != "yes" {
fmt.Println("Aborting login.")
return nil
}
verifier, err := generateRandomString(32)
if err != nil {
return fmt.Errorf("failed to generate verifier: %w", err)
}
challenge := generateCodeChallenge(verifier)

fmt.Println("Opening browser for authentication...")
fmt.Printf("If your browser didn't open, click here: \033]8;;%s\033\\%s\033]8;;\033\\\n", authUrl, authUrl)
u, _ := url.Parse(authUrl)
q := u.Query()
q.Set("client_id", clientID)
q.Set("response_type", "code")
q.Set("redirect_uri", redirectURI)
q.Set("code_challenge", challenge)
q.Set("code_challenge_method", "S256")
u.RawQuery = q.Encode()

err := openUrl()
if err != nil {
fmt.Println("Error opening browser:", err)
return err
fmt.Printf("If your browser doesn't open, visit: %s\n", u.String())

if err := openUrl(u.String()); err != nil {
fmt.Printf("Failed to open browser: %v\n", err)
}
fmt.Print("token: ")

fmt.Print("Enter the authorization code: ")
reader := bufio.NewReader(os.Stdin)
token, _ := reader.ReadString('\n')
token = strings.TrimSpace(token)
code, _ := reader.ReadString('\n')
code = strings.TrimSpace(code)

token, err = getLongLivedToken(token)
if err != nil {
fmt.Println("Error obtaining long-lived token:", err)
return err
if code == "" {
return fmt.Errorf("authorization code is required")
}

err = saveTokenToConfig(ctx, token)
data := url.Values{}
data.Set("grant_type", "authorization_code")
data.Set("client_id", clientID)
data.Set("code", code)
data.Set("redirect_uri", redirectURI)
data.Set("code_verifier", verifier)

resp, err := http.PostForm(tokenUrl, data)
if err != nil {
fmt.Println("Error during token saving")
return err
return fmt.Errorf("failed to exchange token: %w", err)
}
defer resp.Body.Close()

fmt.Println("Saved token to config")
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("token exchange failed: %s", body)
}

return nil
var tokenResp map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return fmt.Errorf("failed to decode token response: %w", err)
}

accessToken, ok := tokenResp["access_token"].(string)
if !ok {
return fmt.Errorf("invalid token response: missing access_token")
}

refreshToken, _ := tokenResp["refresh_token"].(string)
expiresIn, _ := tokenResp["expires_int"].(float64)
tokenType, _ := tokenResp["token_type"].(string)

expiresAt := time.Now().Add(time.Duration(expiresIn) * time.Second)

return saveTokenToConfig(ctx, accessToken)
}
4 changes: 3 additions & 1 deletion cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import (
)

type Config struct {
AccessToken string `json:"access_token"`
AccessToken string `json:"access_token"`
RefreshToken string
ExpiresAt time.Time
// maybe APIBaseURL, etc.
}

Expand Down
Empty file added go.sum
Empty file.
Loading