package config import ( "os" "path/filepath" "runtime" "testing" "time" "allaboutapps.dev/aw/go-starter/internal/mailer/transport" "allaboutapps.dev/aw/go-starter/internal/push/provider" "allaboutapps.dev/aw/go-starter/internal/util" "github.com/rs/zerolog" "golang.org/x/text/language" ) type EchoServer struct { Debug bool ListenAddress string HideInternalServerErrorDetails bool BaseURL string EnableCORSMiddleware bool EnableLoggerMiddleware bool EnableRecoverMiddleware bool EnableRequestIDMiddleware bool EnableTrailingSlashMiddleware bool EnableSecureMiddleware bool EnableCacheControlMiddleware bool SecureMiddleware EchoServerSecureMiddleware WebTemplatesViewsBaseDirAbs string } type PprofServer struct { Enable bool EnableManagementKeyAuth bool RuntimeBlockProfileRate int RuntimeMutexProfileFraction int } // EchoServerSecureMiddleware represents a subset of echo's secure middleware config relevant to the app server. // https://github.com/labstack/echo/blob/master/middleware/secure.go type EchoServerSecureMiddleware struct { XSSProtection string ContentTypeNosniff string XFrameOptions string HSTSMaxAge int HSTSExcludeSubdomains bool ContentSecurityPolicy string CSPReportOnly bool HSTSPreloadEnabled bool ReferrerPolicy string } type AuthServer struct { AccessTokenValidity time.Duration PasswordResetTokenValidity time.Duration PasswordResetTokenDebounceDuration time.Duration PasswordResetTokenReuseDuration time.Duration DefaultUserScopes []string LastAuthenticatedAtThreshold time.Duration RegistrationRequiresConfirmation bool ConfirmationTokenValidity time.Duration ConfirmationTokenDebounceDuration time.Duration } type PathsServer struct { APIBaseDirAbs string MntBaseDirAbs string AppleAppSiteAssociationFile string AndroidAssetlinksFile string } type ManagementServer struct { Secret string `json:"-"` // sensitive ReadinessTimeout time.Duration LivenessTimeout time.Duration ProbeWriteablePathsAbs []string ProbeWriteableTouchfile string EnableMetrics bool } type FrontendServer struct { BaseURL string PasswordResetEndpoint string } type LoggerServer struct { Level zerolog.Level RequestLevel zerolog.Level LogRequestBody bool LogRequestHeader bool LogRequestQuery bool LogResponseBody bool LogResponseHeader bool LogCaller bool PrettyPrintConsole bool } type I18n struct { DefaultLanguage language.Tag BundleDirAbs string } type Server struct { Database Database Echo EchoServer Pprof PprofServer Paths PathsServer Auth AuthServer Management ManagementServer Mailer Mailer SMTP transport.SMTPMailTransportConfig Frontend FrontendServer Logger LoggerServer Push PushService FCMConfig provider.FCMConfig I18n I18n } // DefaultServiceConfigFromEnv returns the server config as parsed from environment variables // and their respective defaults defined below. // We don't expect that ENV_VARs change while we are running our application or our tests // (and it would be a bad thing to do anyways with parallel testing). // Do NOT use os.Setenv / os.Unsetenv in tests utilizing DefaultServiceConfigFromEnv()! func DefaultServiceConfigFromEnv() Server { // An `.env.local` file in your project root can override the currently set ENV variables. // // We never automatically apply `.env.local` when running "go test" as these ENV variables // may be sensitive (e.g. secrets to external APIs) and applying them modifies the process // global "os.Env" state (it should be applied via t.SetEnv instead). // // If you need dotenv ENV variables available in a test, do that explicitly within that // test before executing DefaultServiceConfigFromEnv (or test.WithTestServer). // See /internal/test/helper_dot_env.go: test.DotEnvLoadLocalOrSkipTest(t) if !testing.Testing() { DotEnvTryLoad(filepath.Join(util.GetProjectRootDir(), ".env.local"), os.Setenv) } return Server{ Database: Database{ Host: util.GetEnv("PGHOST", "postgres"), Port: util.GetEnvAsInt("PGPORT", 5432), Database: util.GetEnv("PGDATABASE", "development"), Username: util.GetEnv("PGUSER", "dbuser"), Password: util.GetEnv("PGPASSWORD", ""), AdditionalParams: map[string]string{ "sslmode": util.GetEnv("PGSSLMODE", "disable"), }, MaxOpenConns: util.GetEnvAsInt("DB_MAX_OPEN_CONNS", runtime.NumCPU()*2), MaxIdleConns: util.GetEnvAsInt("DB_MAX_IDLE_CONNS", 1), ConnMaxLifetime: time.Second * time.Duration(util.GetEnvAsInt("DB_CONN_MAX_LIFETIME_SEC", 60)), }, Echo: EchoServer{ Debug: util.GetEnvAsBool("SERVER_ECHO_DEBUG", false), ListenAddress: util.GetEnv("SERVER_ECHO_LISTEN_ADDRESS", ":8080"), HideInternalServerErrorDetails: util.GetEnvAsBool("SERVER_ECHO_HIDE_INTERNAL_SERVER_ERROR_DETAILS", true), BaseURL: util.GetEnv("SERVER_ECHO_BASE_URL", "http://localhost:8080"), EnableCORSMiddleware: util.GetEnvAsBool("SERVER_ECHO_ENABLE_CORS_MIDDLEWARE", true), EnableLoggerMiddleware: util.GetEnvAsBool("SERVER_ECHO_ENABLE_LOGGER_MIDDLEWARE", true), EnableRecoverMiddleware: util.GetEnvAsBool("SERVER_ECHO_ENABLE_RECOVER_MIDDLEWARE", true), EnableRequestIDMiddleware: util.GetEnvAsBool("SERVER_ECHO_ENABLE_REQUEST_ID_MIDDLEWARE", true), EnableTrailingSlashMiddleware: util.GetEnvAsBool("SERVER_ECHO_ENABLE_TRAILING_SLASH_MIDDLEWARE", true), EnableSecureMiddleware: util.GetEnvAsBool("SERVER_ECHO_ENABLE_SECURE_MIDDLEWARE", true), EnableCacheControlMiddleware: util.GetEnvAsBool("SERVER_ECHO_ENABLE_CACHE_CONTROL_MIDDLEWARE", true), // see https://echo.labstack.com/middleware/secure // see https://github.com/labstack/echo/blob/master/middleware/secure.go SecureMiddleware: EchoServerSecureMiddleware{ XSSProtection: util.GetEnv("SERVER_ECHO_SECURE_MIDDLEWARE_XSS_PROTECTION", "1; mode=block"), ContentTypeNosniff: util.GetEnv("SERVER_ECHO_SECURE_MIDDLEWARE_CONTENT_TYPE_NOSNIFF", "nosniff"), XFrameOptions: util.GetEnv("SERVER_ECHO_SECURE_MIDDLEWARE_X_FRAME_OPTIONS", "SAMEORIGIN"), HSTSMaxAge: util.GetEnvAsInt("SERVER_ECHO_SECURE_MIDDLEWARE_HSTS_MAX_AGE", 0), HSTSExcludeSubdomains: util.GetEnvAsBool("SERVER_ECHO_SECURE_MIDDLEWARE_HSTS_EXCLUDE_SUBDOMAINS", false), ContentSecurityPolicy: util.GetEnv("SERVER_ECHO_SECURE_MIDDLEWARE_CONTENT_SECURITY_POLICY", ""), CSPReportOnly: util.GetEnvAsBool("SERVER_ECHO_SECURE_MIDDLEWARE_CSP_REPORT_ONLY", false), HSTSPreloadEnabled: util.GetEnvAsBool("SERVER_ECHO_SECURE_MIDDLEWARE_HSTS_PRELOAD_ENABLED", false), ReferrerPolicy: util.GetEnv("SERVER_ECHO_SECURE_MIDDLEWARE_REFERRER_POLICY", ""), }, WebTemplatesViewsBaseDirAbs: util.GetEnv("SERVER_ECHO_WEB_TEMPLATES_VIEWS_BASE_DIR_ABS", filepath.Join(util.GetProjectRootDir(), "/web/templates/views")), }, Pprof: PprofServer{ // https://golang.org/pkg/net/http/pprof/ Enable: util.GetEnvAsBool("SERVER_PPROF_ENABLE", false), EnableManagementKeyAuth: util.GetEnvAsBool("SERVER_PPROF_ENABLE_MANAGEMENT_KEY_AUTH", true), RuntimeBlockProfileRate: util.GetEnvAsInt("SERVER_PPROF_RUNTIME_BLOCK_PROFILE_RATE", 0), RuntimeMutexProfileFraction: util.GetEnvAsInt("SERVER_PPROF_RUNTIME_MUTEX_PROFILE_FRACTION", 0), }, Paths: PathsServer{ // Please ALWAYS work with ABSOLUTE (ABS) paths from ENV_VARS (however you may resolve a project-relative to absolute for the default value) APIBaseDirAbs: util.GetEnv("SERVER_PATHS_API_BASE_DIR_ABS", filepath.Join(util.GetProjectRootDir(), "/api")), // /app/api (swagger.yml) MntBaseDirAbs: util.GetEnv("SERVER_PATHS_MNT_BASE_DIR_ABS", filepath.Join(util.GetProjectRootDir(), "/assets/mnt")), // /app/assets/mnt (user-generated content) AppleAppSiteAssociationFile: util.GetEnv("SERVER_PATHS_APPLE_APP_SITE_ASSOCIATION_FILE", ""), AndroidAssetlinksFile: util.GetEnv("SERVER_PATHS_ANDROID_ASSETLINKS_FILE", ""), }, Auth: AuthServer{ AccessTokenValidity: time.Second * time.Duration(util.GetEnvAsInt("SERVER_AUTH_ACCESS_TOKEN_VALIDITY", 86400)), PasswordResetTokenValidity: time.Second * time.Duration(util.GetEnvAsInt("SERVER_AUTH_PASSWORD_RESET_TOKEN_VALIDITY", 900)), PasswordResetTokenDebounceDuration: time.Second * time.Duration(util.GetEnvAsInt("SERVER_AUTH_PASSWORD_RESET_TOKEN_DEBOUNCE_DURATION_SECONDS", 60)), PasswordResetTokenReuseDuration: time.Second * time.Duration(util.GetEnvAsInt("SERVER_AUTH_PASSWORD_RESET_TOKEN_REUSE_DURATION_SECONDS", 0)), DefaultUserScopes: util.GetEnvAsStringArr("SERVER_AUTH_DEFAULT_USER_SCOPES", []string{"app"}), LastAuthenticatedAtThreshold: time.Second * time.Duration(util.GetEnvAsInt("SERVER_AUTH_LAST_AUTHENTICATED_AT_THRESHOLD", 900)), RegistrationRequiresConfirmation: util.GetEnvAsBool("SERVER_AUTH_REGISTRATION_REQUIRES_CONFIRMATION", false), ConfirmationTokenValidity: time.Second * time.Duration(util.GetEnvAsInt("SERVER_AUTH_CONFIRMATION_TOKEN_VALIDITY_SECONDS", 86400)), ConfirmationTokenDebounceDuration: time.Second * time.Duration(util.GetEnvAsInt("SERVER_AUTH_CONFIRMATION_TOKEN_DEBOUNCE_DURATION_SECONDS", 60)), }, Management: ManagementServer{ Secret: util.GetMgmtSecret("SERVER_MANAGEMENT_SECRET"), ReadinessTimeout: time.Second * time.Duration(util.GetEnvAsInt("SERVER_MANAGEMENT_READINESS_TIMEOUT_SEC", 4)), LivenessTimeout: time.Second * time.Duration(util.GetEnvAsInt("SERVER_MANAGEMENT_LIVENESS_TIMEOUT_SEC", 9)), ProbeWriteablePathsAbs: util.GetEnvAsStringArr("SERVER_MANAGEMENT_PROBE_WRITEABLE_PATHS_ABS", []string{ filepath.Join(util.GetProjectRootDir(), "/assets/mnt")}, ","), ProbeWriteableTouchfile: util.GetEnv("SERVER_MANAGEMENT_PROBE_WRITEABLE_TOUCHFILE", ".healthy"), EnableMetrics: util.GetEnvAsBool("SERVER_MANAGEMENT_ENABLE_METRICS", false), }, Mailer: Mailer{ DefaultSender: util.GetEnv("SERVER_MAILER_DEFAULT_SENDER", "go-starter@example.com"), Send: util.GetEnvAsBool("SERVER_MAILER_SEND", true), WebTemplatesEmailBaseDirAbs: util.GetEnv("SERVER_MAILER_WEB_TEMPLATES_EMAIL_BASE_DIR_ABS", filepath.Join(util.GetProjectRootDir(), "/web/templates/email")), // /app/web/templates/email Transporter: util.GetEnvEnum("SERVER_MAILER_TRANSPORTER", MailerTransporterMock.String(), []string{MailerTransporterSMTP.String(), MailerTransporterMock.String()}), }, SMTP: transport.SMTPMailTransportConfig{ Host: util.GetEnv("SERVER_SMTP_HOST", "mailhog"), Port: util.GetEnvAsInt("SERVER_SMTP_PORT", 1025), Username: util.GetEnv("SERVER_SMTP_USERNAME", ""), Password: util.GetEnv("SERVER_SMTP_PASSWORD", ""), AuthType: transport.SMTPAuthTypeFromString(util.GetEnv("SERVER_SMTP_AUTH_TYPE", transport.SMTPAuthTypeNone.String())), Encryption: transport.SMTPEncryption(util.GetEnvEnum("SERVER_SMTP_ENCRYPTION", transport.SMTPEncryptionNone.String(), []string{transport.SMTPEncryptionNone.String(), transport.SMTPEncryptionTLS.String(), transport.SMTPEncryptionStartTLS.String()})), TLSConfig: nil, }, Frontend: FrontendServer{ BaseURL: util.GetEnv("SERVER_FRONTEND_BASE_URL", "http://localhost:3000"), PasswordResetEndpoint: util.GetEnv("SERVER_FRONTEND_PASSWORD_RESET_ENDPOINT", "/set-new-password"), }, Logger: LoggerServer{ Level: util.LogLevelFromString(util.GetEnv("SERVER_LOGGER_LEVEL", zerolog.DebugLevel.String())), RequestLevel: util.LogLevelFromString(util.GetEnv("SERVER_LOGGER_REQUEST_LEVEL", zerolog.DebugLevel.String())), LogRequestBody: util.GetEnvAsBool("SERVER_LOGGER_LOG_REQUEST_BODY", false), LogRequestHeader: util.GetEnvAsBool("SERVER_LOGGER_LOG_REQUEST_HEADER", false), LogRequestQuery: util.GetEnvAsBool("SERVER_LOGGER_LOG_REQUEST_QUERY", false), LogResponseBody: util.GetEnvAsBool("SERVER_LOGGER_LOG_RESPONSE_BODY", false), LogResponseHeader: util.GetEnvAsBool("SERVER_LOGGER_LOG_RESPONSE_HEADER", false), LogCaller: util.GetEnvAsBool("SERVER_LOGGER_LOG_CALLER", false), PrettyPrintConsole: util.GetEnvAsBool("SERVER_LOGGER_PRETTY_PRINT_CONSOLE", false), }, Push: PushService{ UseFCMProvider: util.GetEnvAsBool("SERVER_PUSH_USE_FCM", false), UseMockProvider: util.GetEnvAsBool("SERVER_PUSH_USE_MOCK", true), }, FCMConfig: provider.FCMConfig{ GoogleApplicationCredentials: util.GetEnv("GOOGLE_APPLICATION_CREDENTIALS", ""), ProjectID: util.GetEnv("SERVER_FCM_PROJECT_ID", "no-fcm-project-id-set"), ValidateOnly: util.GetEnvAsBool("SERVER_FCM_VALIDATE_ONLY", true), }, I18n: I18n{ DefaultLanguage: util.GetEnvAsLanguageTag("SERVER_I18N_DEFAULT_LANGUAGE", language.English), BundleDirAbs: util.GetEnv("SERVER_I18N_BUNDLE_DIR_ABS", filepath.Join(util.GetProjectRootDir(), "/web/i18n")), // /app/web/i18n }, } }