github.com/supabase/cli@v1.168.1/internal/utils/access_token.go (about)

     1  package utils
     2  
     3  import (
     4  	"os"
     5  	"path/filepath"
     6  	"regexp"
     7  
     8  	"github.com/go-errors/errors"
     9  	"github.com/spf13/afero"
    10  	"github.com/supabase/cli/internal/utils/credentials"
    11  	"github.com/zalando/go-keyring"
    12  )
    13  
    14  var (
    15  	AccessTokenPattern = regexp.MustCompile(`^sbp_[a-f0-9]{40}$`)
    16  	ErrInvalidToken    = errors.New("Invalid access token format. Must be like `sbp_0102...1920`.")
    17  	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"))
    18  	ErrNotLoggedIn     = errors.New("You were not logged in, nothing to do.")
    19  )
    20  
    21  const AccessTokenKey = "access-token"
    22  
    23  func LoadAccessToken() (string, error) {
    24  	return LoadAccessTokenFS(afero.NewOsFs())
    25  }
    26  
    27  func LoadAccessTokenFS(fsys afero.Fs) (string, error) {
    28  	accessToken, err := loadAccessToken(fsys)
    29  	if err != nil {
    30  		return "", err
    31  	}
    32  	if !AccessTokenPattern.MatchString(accessToken) {
    33  		return "", errors.New(ErrInvalidToken)
    34  	}
    35  	return accessToken, nil
    36  }
    37  
    38  func loadAccessToken(fsys afero.Fs) (string, error) {
    39  	// Env takes precedence
    40  	if accessToken := os.Getenv("SUPABASE_ACCESS_TOKEN"); accessToken != "" {
    41  		return accessToken, nil
    42  	}
    43  	// Load from native credentials store
    44  	if accessToken, err := credentials.Get(AccessTokenKey); err == nil {
    45  		return accessToken, nil
    46  	}
    47  	// Fallback to token file
    48  	return fallbackLoadToken(fsys)
    49  }
    50  
    51  func fallbackLoadToken(fsys afero.Fs) (string, error) {
    52  	path, err := getAccessTokenPath()
    53  	if err != nil {
    54  		return "", err
    55  	}
    56  	accessToken, err := afero.ReadFile(fsys, path)
    57  	if errors.Is(err, os.ErrNotExist) {
    58  		return "", errors.New(ErrMissingToken)
    59  	} else if err != nil {
    60  		return "", errors.Errorf("failed to read access token file: %w", err)
    61  	}
    62  	return string(accessToken), nil
    63  }
    64  
    65  func SaveAccessToken(accessToken string, fsys afero.Fs) error {
    66  	// Validate access token
    67  	if !AccessTokenPattern.MatchString(accessToken) {
    68  		return errors.New(ErrInvalidToken)
    69  	}
    70  	// Save to native credentials store
    71  	if err := credentials.Set(AccessTokenKey, accessToken); err == nil {
    72  		return nil
    73  	}
    74  	// Fallback to token file
    75  	return fallbackSaveToken(accessToken, fsys)
    76  }
    77  
    78  func fallbackSaveToken(accessToken string, fsys afero.Fs) error {
    79  	path, err := getAccessTokenPath()
    80  	if err != nil {
    81  		return err
    82  	}
    83  	if err := MkdirIfNotExistFS(fsys, filepath.Dir(path)); err != nil {
    84  		return err
    85  	}
    86  	if err := afero.WriteFile(fsys, path, []byte(accessToken), 0600); err != nil {
    87  		return errors.Errorf("failed to save access token file: %w", err)
    88  	}
    89  	return nil
    90  }
    91  
    92  func DeleteAccessToken(fsys afero.Fs) error {
    93  	// Always delete the fallback token file to handle legacy CLI
    94  	if err := fallbackDeleteToken(fsys); err == nil {
    95  		// Typically user system should only have either token file or keyring.
    96  		// But we delete from both just in case.
    97  		_ = credentials.Delete(AccessTokenKey)
    98  		return nil
    99  	} else if !errors.Is(err, os.ErrNotExist) {
   100  		return err
   101  	}
   102  	// Fallback not found, delete from native credentials store
   103  	err := credentials.Delete(AccessTokenKey)
   104  	if errors.Is(err, credentials.ErrNotSupported) || errors.Is(err, keyring.ErrNotFound) {
   105  		return errors.New(ErrNotLoggedIn)
   106  	} else if err != nil {
   107  		return errors.Errorf("failed to delete access token from keyring: %w", err)
   108  	}
   109  	return nil
   110  }
   111  
   112  func fallbackDeleteToken(fsys afero.Fs) error {
   113  	path, err := getAccessTokenPath()
   114  	if err != nil {
   115  		return err
   116  	}
   117  	if err := fsys.Remove(path); err != nil {
   118  		return errors.Errorf("failed to remove access token file: %w", err)
   119  	}
   120  	return nil
   121  }
   122  
   123  func getAccessTokenPath() (string, error) {
   124  	home, err := os.UserHomeDir()
   125  	if err != nil {
   126  		return "", errors.Errorf("failed to get $HOME directory: %w", err)
   127  	}
   128  	// TODO: fallback to workdir
   129  	return filepath.Join(home, ".supabase", AccessTokenKey), nil
   130  }