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 }