Skip to content
Merged
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
37 changes: 33 additions & 4 deletions internal/appcore/auth_recovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import (
"github.com/HexmosTech/git-lrc/storage"
)

// ErrAuthHandled indicates an auth failure already handled visually to suppress redundant logs.
var ErrAuthHandled = errors.New("authentication failure already handled")

const liveReviewAPIKeyInvalidCode = "LIVE_REVIEW_API_KEY_INVALID"

type createAPIKeyRuntimeRequest struct {
Expand Down Expand Up @@ -100,7 +103,7 @@ func submitReviewWithRecovery(config Config, base64Diff, repoName string, verbos

recoveredConfig, recErr := recoverAPIKeyAndTokens(config, "submit")
if recErr != nil {
return reviewmodel.DiffReviewCreateResponse{}, config, fmt.Errorf("auto-recovery failed after %s: %w", liveReviewAPIKeyInvalidCode, recErr)
return reviewmodel.DiffReviewCreateResponse{}, config, ErrAuthHandled
}

fmt.Println("Retrying review submission with refreshed credentials...")
Expand All @@ -122,7 +125,7 @@ func pollReviewWithRecovery(config Config, reviewID string, pollInterval, timeou

recoveredConfig, recErr := recoverAPIKeyAndTokens(config, "poll")
if recErr != nil {
return nil, config, fmt.Errorf("auto-recovery failed after %s: %w", liveReviewAPIKeyInvalidCode, recErr)
return nil, config, ErrAuthHandled
}

fmt.Println("Retrying review polling with refreshed credentials...")
Expand All @@ -133,6 +136,28 @@ func pollReviewWithRecovery(config Config, reviewID string, pollInterval, timeou
return retryResult, recoveredConfig, nil
}

func highlightCommand(cmd string) string {
return "\033[36m" + cmd + "\033[0m"
}

func printManualReauthInstructions() {
const (
cReset = "\033[0m"
cBold = "\033[1m"
cYellow = "\033[33m"
cDim = "\033[2m"
)

fmt.Println()
fmt.Printf(" %s%s🔐 MANUAL RE-AUTHENTICATION REQUIRED%s\n", cBold, cYellow, cReset)
fmt.Printf(" %s─────────────────────────────────────────────────────%s\n", cDim, cReset)
fmt.Println()
fmt.Printf(" 1. Open the LRC UI by running: %s\n", highlightCommand("lrc ui"))
fmt.Printf(" 2. Click the %sRe-authenticate%s button\n", cBold, cReset)
fmt.Printf(" 3. Once authenticated, run the command again to continue\n")
fmt.Println()
}

func recoverAPIKeyAndTokens(config Config, phase string) (Config, error) {
started := time.Now()
diag := authRecoveryDiagnostic{
Expand All @@ -158,6 +183,7 @@ func recoverAPIKeyAndTokens(config Config, phase string) (Config, error) {
diag.FailureReason = "missing org_id in config"
reportDiagnosticWriteError(persistAuthRecoveryDiagnostic(&diag, time.Since(started)))
fmt.Println("Automatic recovery unavailable: missing org_id in ~/.lrc.toml.")
printManualReauthInstructions()
return config, fmt.Errorf("missing org_id in config")
}

Expand Down Expand Up @@ -186,6 +212,7 @@ func recoverAPIKeyAndTokens(config Config, phase string) (Config, error) {
if createStatus != http.StatusUnauthorized || strings.TrimSpace(config.RefreshToken) == "" {
diag.FailureReason = fmt.Sprintf("create API key failed before refresh: status=%d", createStatus)
reportDiagnosticWriteError(persistAuthRecoveryDiagnostic(&diag, time.Since(started)))
printManualReauthInstructions()
return config, fmt.Errorf("create API key failed: %w body=%s", err, strings.TrimSpace(createBody))
}

Expand All @@ -195,6 +222,7 @@ func recoverAPIKeyAndTokens(config Config, phase string) (Config, error) {
if refreshErr != nil {
diag.FailureReason = fmt.Sprintf("refresh token failed: status=%d", refreshStatus)
reportDiagnosticWriteError(persistAuthRecoveryDiagnostic(&diag, time.Since(started)))
printManualReauthInstructions()
return config, fmt.Errorf("failed to refresh session: %w body=%s", refreshErr, strings.TrimSpace(refreshBody))
}

Expand All @@ -218,9 +246,10 @@ func recoverAPIKeyAndTokens(config Config, phase string) (Config, error) {

newKey, createStatus, createBody, err = createAPIKeyWithJWT(updated.APIURL, updated.OrgID, updated.JWT)
if err != nil {
diag.FailureReason = fmt.Sprintf("create API key after refresh failed: status=%d", createStatus)
diag.FailureReason = fmt.Sprintf("create API key failed after refresh: status=%d", createStatus)
reportDiagnosticWriteError(persistAuthRecoveryDiagnostic(&diag, time.Since(started)))
return config, fmt.Errorf("failed to create API key after refresh: %w body=%s", err, strings.TrimSpace(createBody))
printManualReauthInstructions()
return config, fmt.Errorf("failed to create API key after session refresh: %w body=%s", err, strings.TrimSpace(createBody))
}

updated.APIKey = newKey
Expand Down
9 changes: 7 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package main

import (
"log"
"errors"
"fmt"
"os"

cmdapp "github.com/HexmosTech/git-lrc/cmd"
Expand Down Expand Up @@ -72,7 +73,11 @@ func main() {
})

if err := app.Run(os.Args); err != nil {
log.Fatal(err)
if errors.Is(err, appcore.ErrAuthHandled) {
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

Expand Down
Loading