github.com/Redstoneguy129/cli@v0.0.0-20230211220159-15dca4e91917/internal/utils/config.go (about)

     1  package utils
     2  
     3  import (
     4  	"bytes"
     5  	_ "embed"
     6  	"errors"
     7  	"fmt"
     8  	"github.com/docker/go-units"
     9  	"os"
    10  	"path/filepath"
    11  	"regexp"
    12  	"text/template"
    13  
    14  	"github.com/BurntSushi/toml"
    15  	"github.com/spf13/afero"
    16  )
    17  
    18  var (
    19  	DbImage     string
    20  	NetId       string
    21  	DbId        string
    22  	KongId      string
    23  	GotrueId    string
    24  	InbucketId  string
    25  	RealtimeId  string
    26  	RestId      string
    27  	StorageId   string
    28  	ImgProxyId  string
    29  	DifferId    string
    30  	PgmetaId    string
    31  	StudioId    string
    32  	DenoRelayId string
    33  
    34  	InitialSchemaSql string
    35  	//go:embed templates/initial_schemas/13.sql
    36  	InitialSchemaPg13Sql string
    37  	//go:embed templates/initial_schemas/14.sql
    38  	InitialSchemaPg14Sql string
    39  	//go:embed templates/initial_schemas/15.sql
    40  	InitialSchemaPg15Sql string
    41  
    42  	authExternalProviders = []string{
    43  		"apple",
    44  		"azure",
    45  		"bitbucket",
    46  		"discord",
    47  		"facebook",
    48  		"github",
    49  		"gitlab",
    50  		"google",
    51  		"keycloak",
    52  		"linkedin",
    53  		"notion",
    54  		"twitch",
    55  		"twitter",
    56  		"slack",
    57  		"spotify",
    58  		"workos",
    59  		"zoom",
    60  	}
    61  
    62  	//go:embed templates/init_config.toml
    63  	initConfigEmbed       string
    64  	initConfigTemplate, _ = template.New("initConfig").Parse(initConfigEmbed)
    65  
    66  	//go:embed templates/init_config.test.toml
    67  	testInitConfigEmbed       string
    68  	testInitConfigTemplate, _ = template.New("initConfig.test").Parse(testInitConfigEmbed)
    69  )
    70  
    71  // Type for turning human-friendly bytes string ("5MB", "32kB") into an int64 during toml decoding.
    72  type sizeInBytes int64
    73  
    74  func (s *sizeInBytes) UnmarshalText(text []byte) error {
    75  	size, err := units.RAMInBytes(string(text))
    76  	if err == nil {
    77  		*s = sizeInBytes(size)
    78  	}
    79  	return err
    80  }
    81  
    82  var Config config
    83  
    84  type (
    85  	config struct {
    86  		Hostname  string              `toml:"hostname"`
    87  		ProjectId string              `toml:"project_id"`
    88  		Api       api                 `toml:"api"`
    89  		Db        db                  `toml:"db"`
    90  		Studio    studio              `toml:"studio"`
    91  		Inbucket  inbucket            `toml:"inbucket"`
    92  		Storage   storage             `toml:"storage"`
    93  		Auth      auth                `toml:"auth"`
    94  		Functions map[string]function `toml:"functions"`
    95  		// TODO
    96  		// Scripts   scripts
    97  	}
    98  
    99  	api struct {
   100  		Port            uint     `toml:"port"`
   101  		Schemas         []string `toml:"schemas"`
   102  		ExtraSearchPath []string `toml:"extra_search_path"`
   103  		MaxRows         uint     `toml:"max_rows"`
   104  	}
   105  
   106  	db struct {
   107  		Port         uint `toml:"port"`
   108  		ShadowPort   uint `toml:"shadow_port"`
   109  		MajorVersion uint `toml:"major_version"`
   110  	}
   111  
   112  	studio struct {
   113  		Port uint `toml:"port"`
   114  	}
   115  
   116  	inbucket struct {
   117  		Port     uint `toml:"port"`
   118  		SmtpPort uint `toml:"smtp_port"`
   119  		Pop3Port uint `toml:"pop3_port"`
   120  	}
   121  
   122  	storage struct {
   123  		FileSizeLimit sizeInBytes `toml:"file_size_limit"`
   124  	}
   125  
   126  	auth struct {
   127  		SiteUrl                string   `toml:"site_url"`
   128  		AdditionalRedirectUrls []string `toml:"additional_redirect_urls"`
   129  		JwtExpiry              uint     `toml:"jwt_expiry"`
   130  		EnableSignup           *bool    `toml:"enable_signup"`
   131  		Email                  email    `toml:"email"`
   132  		External               map[string]provider
   133  	}
   134  
   135  	email struct {
   136  		EnableSignup         *bool `toml:"enable_signup"`
   137  		DoubleConfirmChanges *bool `toml:"double_confirm_changes"`
   138  		EnableConfirmations  *bool `toml:"enable_confirmations"`
   139  	}
   140  
   141  	provider struct {
   142  		Enabled     bool   `toml:"enabled"`
   143  		ClientId    string `toml:"client_id"`
   144  		Secret      string `toml:"secret"`
   145  		Url         string `toml:"url"`
   146  		RedirectUri string `toml:"redirect_uri"`
   147  	}
   148  
   149  	function struct {
   150  		VerifyJWT *bool  `toml:"verify_jwt"`
   151  		ImportMap string `toml:"import_map"`
   152  	}
   153  
   154  	// TODO
   155  	// scripts struct {
   156  	// 	BeforeMigrations string `toml:"before_migrations"`
   157  	// 	AfterMigrations  string `toml:"after_migrations"`
   158  	// }
   159  )
   160  
   161  func LoadConfig() error {
   162  	return LoadConfigFS(afero.NewOsFs())
   163  }
   164  
   165  func LoadConfigFS(fsys afero.Fs) error {
   166  	// TODO: provide a config interface for all sub commands to use fsys
   167  	if _, err := toml.DecodeFS(afero.NewIOFS(fsys), ConfigPath, &Config); err != nil {
   168  		CmdSuggestion = fmt.Sprintf("Have you set up the project with %s?", Aqua("supabase init"))
   169  		cwd, osErr := os.Getwd()
   170  		if osErr != nil {
   171  			cwd = "current directory"
   172  		}
   173  		return fmt.Errorf("cannot read config in %s: %w", cwd, err)
   174  	}
   175  
   176  	// Process decoded TOML.
   177  	{
   178  		if Config.ProjectId == "" {
   179  			return errors.New("Missing required field in config: project_id")
   180  		} else {
   181  			NetId = "supabase_network_" + Config.ProjectId
   182  			DbId = "supabase_db_" + Config.ProjectId
   183  			KongId = "supabase_kong_" + Config.ProjectId
   184  			GotrueId = "supabase_auth_" + Config.ProjectId
   185  			InbucketId = "supabase_inbucket_" + Config.ProjectId
   186  			RealtimeId = "realtime-dev.supabase_realtime_" + Config.ProjectId
   187  			RestId = "supabase_rest_" + Config.ProjectId
   188  			StorageId = "supabase_storage_" + Config.ProjectId
   189  			ImgProxyId = "storage_imgproxy_" + Config.ProjectId
   190  			DifferId = "supabase_differ_" + Config.ProjectId
   191  			PgmetaId = "supabase_pg_meta_" + Config.ProjectId
   192  			StudioId = "supabase_studio_" + Config.ProjectId
   193  			DenoRelayId = "supabase_deno_relay_" + Config.ProjectId
   194  		}
   195  		if Config.Hostname == "" {
   196  			Config.Hostname = "localhost"
   197  		}
   198  		if Config.Api.Port == 0 {
   199  			return errors.New("Missing required field in config: api.port")
   200  		}
   201  		if Config.Api.MaxRows == 0 {
   202  			Config.Api.MaxRows = 1000
   203  		}
   204  		if len(Config.Api.Schemas) == 0 {
   205  			Config.Api.Schemas = []string{"public", "storage", "graphql_public"}
   206  		}
   207  		// Append required schemas if they are missing
   208  		Config.Api.Schemas = removeDuplicates(append([]string{"public", "storage"}, Config.Api.Schemas...))
   209  		Config.Api.ExtraSearchPath = removeDuplicates(append([]string{"public"}, Config.Api.ExtraSearchPath...))
   210  		if Config.Db.Port == 0 {
   211  			return errors.New("Missing required field in config: db.port")
   212  		}
   213  		if Config.Db.ShadowPort == 0 {
   214  			Config.Db.ShadowPort = 54320
   215  		}
   216  		switch Config.Db.MajorVersion {
   217  		case 0:
   218  			return errors.New("Missing required field in config: db.major_version")
   219  		case 12:
   220  			return errors.New("Postgres version 12.x is unsupported. To use the CLI, either start a new project or follow project migration steps here: https://supabase.com/docs/guides/database#migrating-between-projects.")
   221  		case 13:
   222  			DbImage = Pg13Image
   223  			InitialSchemaSql = InitialSchemaPg13Sql
   224  		case 14:
   225  			DbImage = Pg14Image
   226  			InitialSchemaSql = InitialSchemaPg14Sql
   227  		case 15:
   228  			DbImage = Pg15Image
   229  			InitialSchemaSql = InitialSchemaPg15Sql
   230  		default:
   231  			return fmt.Errorf("Failed reading config: Invalid %s: %v.", Aqua("db.major_version"), Config.Db.MajorVersion)
   232  		}
   233  		if Config.Studio.Port == 0 {
   234  			return errors.New("Missing required field in config: studio.port")
   235  		}
   236  		if Config.Inbucket.Port == 0 {
   237  			return errors.New("Missing required field in config: inbucket.port")
   238  		}
   239  		if Config.Storage.FileSizeLimit == 0 {
   240  			Config.Storage.FileSizeLimit = 50 * units.MiB
   241  		}
   242  		if Config.Auth.SiteUrl == "" {
   243  			return errors.New("Missing required field in config: auth.site_url")
   244  		}
   245  		if Config.Auth.JwtExpiry == 0 {
   246  			Config.Auth.JwtExpiry = 3600
   247  		}
   248  		if Config.Auth.EnableSignup == nil {
   249  			x := true
   250  			Config.Auth.EnableSignup = &x
   251  		}
   252  		if Config.Auth.Email.EnableSignup == nil {
   253  			x := true
   254  			Config.Auth.Email.EnableSignup = &x
   255  		}
   256  		if Config.Auth.Email.DoubleConfirmChanges == nil {
   257  			x := true
   258  			Config.Auth.Email.DoubleConfirmChanges = &x
   259  		}
   260  		if Config.Auth.Email.EnableConfirmations == nil {
   261  			x := true
   262  			Config.Auth.Email.EnableConfirmations = &x
   263  		}
   264  		if Config.Auth.External == nil {
   265  			Config.Auth.External = map[string]provider{}
   266  		}
   267  
   268  		for _, ext := range authExternalProviders {
   269  			if _, ok := Config.Auth.External[ext]; !ok {
   270  				Config.Auth.External[ext] = provider{
   271  					Enabled:  false,
   272  					ClientId: "",
   273  					Secret:   "",
   274  				}
   275  			} else if Config.Auth.External[ext].Enabled {
   276  				maybeLoadEnv := func(s string) (string, error) {
   277  					matches := regexp.MustCompile(`^env\((.*)\)$`).FindStringSubmatch(s)
   278  					if len(matches) == 0 {
   279  						return s, nil
   280  					}
   281  
   282  					envName := matches[1]
   283  					value := os.Getenv(envName)
   284  					if value == "" {
   285  						return "", errors.New(`Error evaluating "env(` + envName + `)": environment variable ` + envName + " is unset.")
   286  					}
   287  
   288  					return value, nil
   289  				}
   290  
   291  				var clientId, secret, redirectUri, url string
   292  
   293  				if Config.Auth.External[ext].ClientId == "" {
   294  					return fmt.Errorf("Missing required field in config: auth.external.%s.client_id", ext)
   295  				} else {
   296  					v, err := maybeLoadEnv(Config.Auth.External[ext].ClientId)
   297  					if err != nil {
   298  						return err
   299  					}
   300  					clientId = v
   301  				}
   302  				if Config.Auth.External[ext].Secret == "" {
   303  					return fmt.Errorf("Missing required field in config: auth.external.%s.secret", ext)
   304  				} else {
   305  					v, err := maybeLoadEnv(Config.Auth.External[ext].Secret)
   306  					if err != nil {
   307  						return err
   308  					}
   309  					secret = v
   310  				}
   311  
   312  				if Config.Auth.External[ext].RedirectUri != "" {
   313  					v, err := maybeLoadEnv(Config.Auth.External[ext].RedirectUri)
   314  					if err != nil {
   315  						return err
   316  					}
   317  					redirectUri = v
   318  				}
   319  
   320  				if Config.Auth.External[ext].Url != "" {
   321  					v, err := maybeLoadEnv(Config.Auth.External[ext].Url)
   322  					if err != nil {
   323  						return err
   324  					}
   325  					url = v
   326  				}
   327  
   328  				Config.Auth.External[ext] = provider{
   329  					Enabled:     true,
   330  					ClientId:    clientId,
   331  					Secret:      secret,
   332  					RedirectUri: redirectUri,
   333  					Url:         url,
   334  				}
   335  			}
   336  		}
   337  	}
   338  
   339  	if Config.Functions == nil {
   340  		Config.Functions = map[string]function{}
   341  	}
   342  	for name, functionConfig := range Config.Functions {
   343  		verifyJWT := functionConfig.VerifyJWT
   344  
   345  		if verifyJWT == nil {
   346  			x := true
   347  			verifyJWT = &x
   348  		}
   349  
   350  		Config.Functions[name] = function{
   351  			VerifyJWT: verifyJWT,
   352  			ImportMap: functionConfig.ImportMap,
   353  		}
   354  	}
   355  
   356  	return nil
   357  }
   358  
   359  func WriteConfig(fsys afero.Fs, test bool) error {
   360  	// Using current directory name as project id
   361  	cwd, err := os.Getwd()
   362  	if err != nil {
   363  		return err
   364  	}
   365  	dir := filepath.Base(cwd)
   366  
   367  	var initConfigBuf bytes.Buffer
   368  	var tmpl *template.Template
   369  	if test {
   370  		tmpl = testInitConfigTemplate
   371  	} else {
   372  		tmpl = initConfigTemplate
   373  	}
   374  
   375  	if err := tmpl.Execute(
   376  		&initConfigBuf,
   377  		struct{ ProjectId string }{ProjectId: dir},
   378  	); err != nil {
   379  		return err
   380  	}
   381  
   382  	if err := MkdirIfNotExistFS(fsys, filepath.Dir(ConfigPath)); err != nil {
   383  		return err
   384  	}
   385  
   386  	if err := afero.WriteFile(fsys, ConfigPath, initConfigBuf.Bytes(), 0644); err != nil {
   387  		return err
   388  	}
   389  
   390  	return nil
   391  }
   392  
   393  func removeDuplicates(slice []string) (result []string) {
   394  	set := make(map[string]struct{})
   395  	for _, item := range slice {
   396  		if _, exists := set[item]; !exists {
   397  			set[item] = struct{}{}
   398  			result = append(result, item)
   399  		}
   400  	}
   401  	return result
   402  }