Skip to content

Commit

Permalink
chore: working prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
karl-cardenas-coding committed Jun 28, 2024
1 parent 64b8c75 commit f628c7a
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 38 deletions.
137 changes: 114 additions & 23 deletions cmd/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ package cmd

import (
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/exec"
"runtime"
"text/template"
"time"

"github.com/karl-cardenas-coding/mywhoop/internal"
"github.com/spf13/cobra"
"golang.org/x/oauth2"
)

// loginCmd represents the login command
var loginCmd = &cobra.Command{
Use: "login",
Short: "Authenticate with Whoop API and get an access token",
Expand All @@ -25,65 +28,124 @@ var loginCmd = &cobra.Command{
},
}

var noAutoOpenBrowser bool

func init() {
loginCmd.PersistentFlags().BoolVarP(&noAutoOpenBrowser, "no-auto", "n", false, "Do not automatically open the browser to authenticate with the Whoop API.")
rootCmd.AddCommand(loginCmd)
}

// PageData is the data structure for the HTML template
type PageData struct {
// AuthURL is the URL to authenticate with the Whoop API
AuthURL string
}

// login authenticates with Whoop API and gets an access token
func login() error {
err := InitLogger(&Configuration)
if err != nil {
return err
}

cliCfg := Configuration

id := os.Getenv("WHOOP_CLIENT_ID")
secret := os.Getenv("WHOOP_CLIENT_SECRET")

if id == "" || secret == "" {
return errors.New("the required env variables WHOOP_CLIENT_ID and WHOOP_CLIENT_SECRET are not set")
}

// cfg := Configuration
auth := internal.AuthRequest{
ClientID: id,
ClientSecret: secret,
AuthorizationURL: internal.DEFAULT_AUTHENTICATION_URL,
TokenURL: internal.DEFAULT_ACCESS_TOKEN_URL,
config := &oauth2.Config{
ClientID: id,
ClientSecret: secret,
RedirectURL: "http://localhost:8080/redirect",
Scopes: []string{
"offline",
"read:recovery",
"read:cycles",
"read:workout",
"read:sleep",
"read:profile",
"read:body_measurement",
},
Endpoint: oauth2.Endpoint{
AuthURL: internal.DEFAULT_AUTHENTICATION_URL,
TokenURL: internal.DEFAULT_ACCESS_TOKEN_URL,
},
}

authUrl := internal.GetAuthURL(*config)

slog.Info("Starting login application helper")
fs := http.FileServer(http.Dir("html/static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))

landingPageHandler := func(w http.ResponseWriter, r *http.Request) {
tmp, err := template.ParseFiles("html/index.html")
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
slog.Error("unable to parse template", "error", err)
}

data := PageData{
AuthURL: authUrl,
}

tmpl := template.Must(tmp, err)
err = tmpl.Execute(w, auth)
err = tmpl.Execute(w, data)
if err != nil {
slog.Error("unable to execute template", "error", err)
}

}

submitHandler := func(w http.ResponseWriter, r *http.Request) {
username := r.FormValue("username")
password := r.FormValue("password")

slog.Info("Username and password received", "username", username, "password", password)
rsp, err := w.Write([]byte(`<div class="container">
<div class="message">
<p>You have successfully authenticated with the Whoop API 🎉.</p>
<p>A file was created in the local directory titled <strong>token.json</strong>. Use the button below to close the application.</p> <p> ⚠️ You must manually close this window - Sorry browser security settings 🔐</p>
</div>
<button hx-post="/close" hx-trigger="click" class="close-button">Close CLI Application</button>
</div>`))
redirectHandler := func(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
slog.Debug("Code received", "code", code)

// Exchange response code for token
accessToken, err := internal.GetAccessToken(*config, code)
if err != nil {
slog.Error("unable to write response", "error", err)
slog.Error("unable to get access token", "error", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}

err = internal.WriteLocalToken(cliCfg.Credentials.CredentialsFile, accessToken)
if err != nil {
slog.Error("unable to write token to file", "error", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
slog.Info("Response written", "response", rsp)

_, err = w.Write([]byte(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MyWhoop Token Helper</title>
<script src="/static/dependencies/htmx.min.js"></script>
<link rel="stylesheet" href="/static/styles/styles.css">
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<link rel="manifest" href="/static/site.webmanifest">
</head>
<body>
<div class="container">
<div class="message">
<p>You have successfully authenticated with the Whoop API 🎉.</p>
<p>A file was created in the specified credentials file path titled <strong>token.json</strong>. Use the button below to close the application.</p> <p> ⚠️ You must manually close this window - Sorry browser security settings 🔐</p>
</div>
<button hx-post="/close" hx-trigger="click" class="close-button">Close CLI Application</button>
</div>
</body>
</html>`))
if err != nil {
slog.Error("unable to write response", "error", err)
}
}

closeAppHandler := func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -95,14 +157,43 @@ func login() error {
}

http.HandleFunc("/", landingPageHandler)
http.HandleFunc("/submit", submitHandler)
http.HandleFunc("/close", closeAppHandler)
http.HandleFunc("/redirect", redirectHandler)

slog.Info("Listening on port 8080. Visit http://localhost:8080 to autenticate with the Whoop API and get an access token.")
err = openBrowser("http://localhost:8080", noAutoOpenBrowser)
if err != nil {
return err
}
err = http.ListenAndServe(":8080", nil)
if err != nil {
return err
}

return nil
}

func openBrowser(url string, disableCmd bool) error {
var cmd string
var args []string

if disableCmd {
return nil
}

switch runtime.GOOS {
case "linux":
cmd = "xdg-open"
args = []string{url}
case "windows":
cmd = "rundll32"
args = []string{"url.dll,FileProtocolHandler", url}
case "darwin":
cmd = "open"
args = []string{url}
default:
return fmt.Errorf("unsupported platform")
}

return exec.Command(cmd, args...).Start()
}
16 changes: 4 additions & 12 deletions html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,10 @@
</head>
<body>
<div class="container" role="main" aria-labelledby="loginTitle" id="main-container">
<h1 id="loginTitle">Login</h1>
<form action="/submit" method="POST" aria-describedby="loginDescription">
<p id="loginDescription">Enter your Whoop username and password to log in.</p>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required aria-required="true">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required aria-required="true">
</div>
<button type="submit" hx-post="/submit" hx-trigger="click" hx-swap="outerHTML" hx-target="#main-container">Submit</button>
<h1 id="loginTitle">Authentication</h1>
<form action="{{.AuthURL}}" method="POST" aria-describedby="loginDescription">
<p aria-label="Click Login to start the authentication process" id="loginDescription">Click Login to start the authentication process 🔒</p>
<button type="submit">Login</button>
</form>
</div>
</body>
Expand Down
3 changes: 2 additions & 1 deletion html/static/styles/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ input[type="password"] {
border-radius: 5px; /* Rounded corners for the input fields */
}


/* Button styles */
button {
padding: 10px 20px;
Expand Down Expand Up @@ -102,4 +103,4 @@ p {
.close-button {
align-self: flex-start; /* Align button to the start (left) */
width: auto; /* Allow button to size based on content */
}
}
13 changes: 12 additions & 1 deletion internal/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ func getEndpoint() oauth2.Endpoint {
}
}

// GetAuthURL returns the URL to authenticate with the Whoop API
func GetAuthURL(auth oauth2.Config) string {

return auth.AuthCodeURL("stateidentifier", oauth2.AccessTypeOffline)
}

// GetAccessToken exchanges the access code returned from the authorization flow for an access token
func GetAccessToken(auth oauth2.Config, code string) (*oauth2.Token, error) {
return auth.Exchange(context.Background(), code)
}

// RefreshToken refreshes the access token
func RefreshToken(ctx context.Context, auth AuthRequest) (oauth2.Token, error) {

Expand Down Expand Up @@ -83,7 +94,7 @@ func RefreshToken(ctx context.Context, auth AuthRequest) (oauth2.Token, error) {
}

// writeLocalToken creates file containing the Whoop authentication token
func writeLocalToken(filePath string, token *oauth2.Token) error {
func WriteLocalToken(filePath string, token *oauth2.Token) error {

f, err := os.Create(filePath)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ func TestWriteLocalToken(t *testing.T) {

filePath := filepath.Join(test.tokenPath, "token.json")

err := writeLocalToken(filePath, &test.token)
err := WriteLocalToken(filePath, &test.token)
if !test.errorExpected && err != nil {
t.Errorf("Test Case - %d: Failed to write token to file: %v", test.id, err)
}
Expand Down

0 comments on commit f628c7a

Please sign in to comment.