tremble/auth/auth.go

222 lines
4.4 KiB
Go
Raw Normal View History

2025-07-31 15:27:22 +00:00
package auth
import (
"encoding/json"
"github.com/nicklaw5/helix/v2"
"context"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"time"
)
// TODO; Handle scopes properly
type Auth struct {
HaveAuth bool
client *helix.Client
context *context.Context
}
var authTimeout time.Duration = time.Second * 60
func New(ctx *context.Context, c *helix.Client, s []string) (*Auth, error) {
var auth *Auth = &Auth{context: ctx, client: c}
// See about getting a token
c.SetRedirectURI("http://localhost:3000")
// See if there are any stored credentials.
creds, err := getUserToken()
if err != nil {
// Caller needs to get followup to get credentials via StartAuth()
return auth, nil
}
// Validate token in case what we've got on file is still good.
validate := func() error {
valid, resp, err := c.ValidateToken(creds.AccessToken)
if valid {
return nil
}
if err != nil {
log.Printf("Validation error: %v\n", err)
}
_ = resp
return err
}
// Attempt to refresh the token if possible.
refresh := func() (*helix.AccessCredentials, error) {
resp, err := c.RefreshUserAccessToken(creds.RefreshToken)
if err != nil {
log.Printf("Error refreshing token: %v\n", err)
return nil, err
}
return &resp.Data, nil
}
// FIXME: This should return an error.
if err := validate(); err != nil {
return auth, err
}
refreshedToken, err := refresh()
if err != nil {
return auth, err
}
// XXX: Maybe wrap this in a function.
c.SetUserAccessToken(refreshedToken.AccessToken)
c.SetRefreshToken(refreshedToken.RefreshToken)
storeUserToken(refreshedToken)
auth.HaveAuth = true
return auth, nil
}
// Returns path to a token file. Creates it if it does nt exist.
func tokenFilePath() (string, error) {
storePath, err := os.UserCacheDir()
if err != nil {
return "", err
}
storePath = filepath.Join(storePath, "tremble")
err = os.Mkdir(storePath, 0755)
if err != nil && !os.IsExist(err) {
log.Printf("Error creating %s: %v\n", storePath, err)
return "", err
}
return filepath.Join(storePath, "token.json"), nil
}
// Only reporting here for now since it's probably not worth trying to recover
// from an error here.
func storeUserToken(token *helix.AccessCredentials) {
tf, err := tokenFilePath()
if err != nil {
return
}
b, err := json.Marshal(token)
if err != nil {
log.Printf("Couldn't marshal %+v: %v\n", token, err)
return
}
err = os.WriteFile(tf, b, 0644)
if err != nil {
log.Printf("Error writing token: %v\n", err)
return
}
}
func getUserToken() (creds helix.AccessCredentials, err error) {
tf, err := tokenFilePath()
if err != nil {
return
}
b, err := os.ReadFile(tf)
if err != nil {
if err != os.ErrNotExist {
log.Printf("Couldn't read token file: %v\n", err)
}
return
}
if err = json.Unmarshal(b, &creds); err != nil {
log.Printf("Error unmarshalling token: %v\n", err)
return
}
return
}
func (a *Auth) updateCode(code string) error {
resp, err := a.client.RequestUserAccessToken(code)
if err != nil {
log.Printf("error requesting user access token: %v\n", err)
return err
}
log.Printf("%+v\n", resp.Data)
a.client.SetUserAccessToken(resp.Data.AccessToken)
storeUserToken(&resp.Data)
return nil
}
func (a *Auth) StartAuth(userId string, s []string) (chan int, error) {
// Need to get authorization
authUrl := a.client.GetAuthorizationURL(&helix.AuthorizationURLParams{
ResponseType: "code",
Scopes: s,
State: userId,
ForceVerify: false,
})
// TODO: Check for MacOS here.
// Open browser and do the auth. Let the auth listener take care of the
// rest.
cmd := exec.Command("xdg-open", authUrl)
if err := cmd.Run(); err != nil {
return nil, err
}
notify := make(chan int)
a.waitForAuth(notify)
return notify, nil
}
func (a *Auth) waitForAuth(notify chan int) {
authCtx, cancel := context.WithTimeout(*a.context, authTimeout)
srv := &http.Server{Addr: ":3000"}
var code string = ""
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
select {
case <-authCtx.Done():
// Authenticated or timed out
return
default:
// Just continue
}
code = r.URL.Query().Get("code")
if code != "" {
cancel()
err := srv.Shutdown(authCtx)
if err != nil {
log.Fatalf("Error shutting down auth server: %v\n", err)
}
a.updateCode(code)
notify <- 1
}
})
go func() {
defer srv.Close()
err := http.ListenAndServe(":3000", nil)
if err != nil {
log.Fatalf("Error running auth server: %v\n", err)
}
}()
}