supabase-cli/internal/utils/access_token.go

131 lines
3.8 KiB
Go

package utils
import (
"os"
"path/filepath"
"regexp"
"github.com/go-errors/errors"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/utils/credentials"
"github.com/zalando/go-keyring"
)
var (
AccessTokenPattern = regexp.MustCompile(`^sbp_(oauth_)?[a-f0-9]{40}$`)
ErrInvalidToken = errors.New("Invalid access token format. Must be like `sbp_0102...1920`.")
ErrMissingToken = errors.Errorf("Access token not provided. Supply an access token by running %s or setting the SUPABASE_ACCESS_TOKEN environment variable.", Aqua("supabase login"))
ErrNotLoggedIn = errors.New("You were not logged in, nothing to do.")
)
const AccessTokenKey = "access-token"
func LoadAccessToken() (string, error) {
return LoadAccessTokenFS(afero.NewOsFs())
}
func LoadAccessTokenFS(fsys afero.Fs) (string, error) {
accessToken, err := loadAccessToken(fsys)
if err != nil {
return "", err
}
if !AccessTokenPattern.MatchString(accessToken) {
return "", errors.New(ErrInvalidToken)
}
return accessToken, nil
}
func loadAccessToken(fsys afero.Fs) (string, error) {
// Env takes precedence
if accessToken := os.Getenv("SUPABASE_ACCESS_TOKEN"); accessToken != "" {
return accessToken, nil
}
// Load from native credentials store
if accessToken, err := credentials.StoreProvider.Get(AccessTokenKey); err == nil {
return accessToken, nil
}
// Fallback to token file
return fallbackLoadToken(fsys)
}
func fallbackLoadToken(fsys afero.Fs) (string, error) {
path, err := getAccessTokenPath()
if err != nil {
return "", err
}
accessToken, err := afero.ReadFile(fsys, path)
if errors.Is(err, os.ErrNotExist) {
return "", errors.New(ErrMissingToken)
} else if err != nil {
return "", errors.Errorf("failed to read access token file: %w", err)
}
return string(accessToken), nil
}
func SaveAccessToken(accessToken string, fsys afero.Fs) error {
// Validate access token
if !AccessTokenPattern.MatchString(accessToken) {
return errors.New(ErrInvalidToken)
}
// Save to native credentials store
if err := credentials.StoreProvider.Set(AccessTokenKey, accessToken); err == nil {
return nil
}
// Fallback to token file
return fallbackSaveToken(accessToken, fsys)
}
func fallbackSaveToken(accessToken string, fsys afero.Fs) error {
path, err := getAccessTokenPath()
if err != nil {
return err
}
if err := MkdirIfNotExistFS(fsys, filepath.Dir(path)); err != nil {
return err
}
if err := afero.WriteFile(fsys, path, []byte(accessToken), 0600); err != nil {
return errors.Errorf("failed to save access token file: %w", err)
}
return nil
}
func DeleteAccessToken(fsys afero.Fs) error {
// Always delete the fallback token file to handle legacy CLI
if err := fallbackDeleteToken(fsys); err == nil {
// Typically user system should only have either token file or keyring.
// But we delete from both just in case.
_ = credentials.StoreProvider.Delete(AccessTokenKey)
return nil
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
// Fallback not found, delete from native credentials store
err := credentials.StoreProvider.Delete(AccessTokenKey)
if errors.Is(err, credentials.ErrNotSupported) || errors.Is(err, keyring.ErrNotFound) {
return errors.New(ErrNotLoggedIn)
} else if err != nil {
return errors.Errorf("failed to delete access token from keyring: %w", err)
}
return nil
}
func fallbackDeleteToken(fsys afero.Fs) error {
path, err := getAccessTokenPath()
if err != nil {
return err
}
if err := fsys.Remove(path); err != nil {
return errors.Errorf("failed to remove access token file: %w", err)
}
return nil
}
func getAccessTokenPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", errors.Errorf("failed to get $HOME directory: %w", err)
}
// TODO: fallback to workdir
return filepath.Join(home, ".supabase", AccessTokenKey), nil
}