(Feat): Initial Commit
This commit is contained in:
18
internal/config/build_args.go
Normal file
18
internal/config/build_args.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package config
|
||||
|
||||
import "fmt"
|
||||
|
||||
// The following vars are automatically injected via -ldflags.
|
||||
// See Makefile target "make go-build" and make var $(LDFLAGS).
|
||||
// No need to change them here.
|
||||
// https://www.digitalocean.com/community/tutorials/using-ldflags-to-set-version-information-for-go-applications
|
||||
var (
|
||||
ModuleName = "build.local/misses/ldflags" // e.g. "allaboutapps.dev/aw/go-starter"
|
||||
Commit = "< 40 chars git commit hash via ldflags >" // e.g. "59cb7684dd0b0f38d68cd7db657cb614feba8f7e"
|
||||
BuildDate = "1970-01-01T00:00:00+00:00" // e.g. "1970-01-01T00:00:00+00:00"
|
||||
)
|
||||
|
||||
// GetFormattedBuildArgs returns string representation of buildsargs set via ldflags "<ModuleName> @ <Commit> (<BuildDate>)"
|
||||
func GetFormattedBuildArgs() string {
|
||||
return fmt.Sprintf("%v @ %v (%v)", ModuleName, Commit, BuildDate)
|
||||
}
|
||||
57
internal/config/db_config.go
Normal file
57
internal/config/db_config.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||
)
|
||||
|
||||
// The DatabaseMigrationTable name is baked into the binary
|
||||
// This setting should always be in sync with dbconfig.yml, sqlboiler.toml and the live database (e.g. to be able to test producation dumps locally)
|
||||
const DatabaseMigrationTable = "migrations"
|
||||
|
||||
// The DatabaseMigrationFolder (folder with all *.sql migrations).
|
||||
// This settings should always be in sync with dbconfig.yaml and Dockerfile (the final app stage).
|
||||
// It's expected that the migrations folder lives at the root of this project or right next to the app binary.
|
||||
var DatabaseMigrationFolder = filepath.Join(util.GetProjectRootDir(), "/migrations")
|
||||
|
||||
type Database struct {
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string `json:"-"` // sensitive
|
||||
Database string
|
||||
AdditionalParams map[string]string `json:"additionalParams,omitempty"` // Optional additional connection parameters mapped into the connection string
|
||||
MaxOpenConns int
|
||||
MaxIdleConns int
|
||||
ConnMaxLifetime time.Duration
|
||||
}
|
||||
|
||||
// ConnectionString generates a connection string to be passed to sql.Open or equivalents, assuming Postgres syntax
|
||||
func (c Database) ConnectionString() string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString(fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s", c.Host, c.Port, c.Username, c.Password, c.Database))
|
||||
|
||||
if _, ok := c.AdditionalParams["sslmode"]; !ok {
|
||||
builder.WriteString(" sslmode=disable")
|
||||
}
|
||||
|
||||
if len(c.AdditionalParams) > 0 {
|
||||
params := make([]string, 0, len(c.AdditionalParams))
|
||||
for param := range c.AdditionalParams {
|
||||
params = append(params, param)
|
||||
}
|
||||
|
||||
sort.Strings(params)
|
||||
|
||||
for _, param := range params {
|
||||
fmt.Fprintf(&builder, " %s=%s", param, c.AdditionalParams[param])
|
||||
}
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
68
internal/config/db_config_test.go
Normal file
68
internal/config/db_config_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||
)
|
||||
|
||||
// via https://github.com/allaboutapps/integresql/blob/master/pkg/manager/database_config_test.go
|
||||
func TestDatabaseConnectionString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config config.Database
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Simple",
|
||||
config: config.Database{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
Username: "simple",
|
||||
Password: "database_config",
|
||||
Database: "simple_database_config",
|
||||
},
|
||||
want: "host=localhost port=5432 user=simple password=database_config dbname=simple_database_config sslmode=disable",
|
||||
},
|
||||
{
|
||||
name: "SSLMode",
|
||||
config: config.Database{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
Username: "simple",
|
||||
Password: "database_config",
|
||||
Database: "simple_database_config",
|
||||
AdditionalParams: map[string]string{
|
||||
"sslmode": "prefer",
|
||||
},
|
||||
},
|
||||
want: "host=localhost port=5432 user=simple password=database_config dbname=simple_database_config sslmode=prefer",
|
||||
},
|
||||
{
|
||||
name: "Complex",
|
||||
config: config.Database{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
Username: "simple",
|
||||
Password: "database_config",
|
||||
Database: "simple_database_config",
|
||||
AdditionalParams: map[string]string{
|
||||
"connect_timeout": "10",
|
||||
"sslmode": "verify-full",
|
||||
"sslcert": "/app/certs/pg.pem",
|
||||
"sslkey": "/app/certs/pg.key",
|
||||
"sslrootcert": "/app/certs/pg_root.pem",
|
||||
},
|
||||
},
|
||||
want: "host=localhost port=5432 user=simple password=database_config dbname=simple_database_config connect_timeout=10 sslcert=/app/certs/pg.pem sslkey=/app/certs/pg.key sslmode=verify-full sslrootcert=/app/certs/pg_root.pem",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.config.ConnectionString(); got != tt.want {
|
||||
t.Errorf("invalid connection string, got %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
71
internal/config/dot_env.go
Normal file
71
internal/config/dot_env.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/subosito/gotenv"
|
||||
)
|
||||
|
||||
// os.SetEnv func signature
|
||||
type envSetter = func(key string, value string) error
|
||||
|
||||
// DotEnvTryLoad forcefully overrides ENV variables through **a maybe available** .env file.
|
||||
//
|
||||
// This function will always remain silent if a .env file does not exist!
|
||||
// If we successfully apply an ENV file, we will log a warning.
|
||||
// If there are any other errors, we will panic!
|
||||
//
|
||||
// This mechanism should only be used **locally** to easily inject (gitignored)
|
||||
// secrets into your ENV. Non-existing .env files are actually the **best case**.
|
||||
//
|
||||
// When running normally (not within tests):
|
||||
// DotEnvTryLoad("/path/tp/my.env.local", os.SetEnv)
|
||||
//
|
||||
// For tests (and autoreset) use t.Setenv:
|
||||
// DotEnvTryLoad("/path/to/my.env.test.local", func(k string, v string) error { t.Setenv(k, v); return nil })
|
||||
func DotEnvTryLoad(absolutePathToEnvFile string, setEnvFn envSetter) {
|
||||
err := DotEnvLoad(absolutePathToEnvFile, setEnvFn)
|
||||
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
log.Panic().Err(err).Str("envFile", absolutePathToEnvFile).Msg(".env parse error!")
|
||||
}
|
||||
} else {
|
||||
log.Warn().Str("envFile", absolutePathToEnvFile).Msg(".env overrides ENV variables!")
|
||||
}
|
||||
}
|
||||
|
||||
// DotEnvLoad forcefully overrides ENV variables through the supplied .env file.
|
||||
//
|
||||
// This mechanism should only be used **locally** to easily inject (gitignored)
|
||||
// secrets into your ENV.
|
||||
//
|
||||
// When running normally (not within tests):
|
||||
// DotEnvLoad("/path/to/my.env.local", os.SetEnv)
|
||||
//
|
||||
// For tests (and ENV var autoreset) use t.Setenv:
|
||||
// DotEnvLoad("/path/to/my.env.test.local", func(k string, v string) error { t.Setenv(k, v); return nil })
|
||||
func DotEnvLoad(absolutePathToEnvFile string, setEnvFn envSetter) error {
|
||||
file, err := os.Open(absolutePathToEnvFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open .env file: %w", err)
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
envs, err := gotenv.StrictParse(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse .env file: %w", err)
|
||||
}
|
||||
|
||||
for key, value := range envs {
|
||||
if err := setEnvFn(key, value); err != nil {
|
||||
return fmt.Errorf("failed to set environment variable: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
63
internal/config/dot_env_test.go
Normal file
63
internal/config/dot_env_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDotEnvOverride(t *testing.T) {
|
||||
assert.Empty(t, os.Getenv("IS_THIS_A_TEST_ENV"))
|
||||
|
||||
orgPsqlUser := os.Getenv("PSQL_USER")
|
||||
|
||||
config.DotEnvTryLoad(
|
||||
filepath.Join(util.GetProjectRootDir(), "/internal/config/testdata/.env1.local"),
|
||||
func(k string, v string) error { t.Setenv(k, v); return nil })
|
||||
|
||||
assert.Equal(t, "yes", os.Getenv("IS_THIS_A_TEST_ENV"))
|
||||
assert.Equal(t, "dotenv_override_psql_user", os.Getenv("PSQL_USER"))
|
||||
assert.Equal(t, orgPsqlUser, os.Getenv("ORIGINAL_PSQL_USER"))
|
||||
|
||||
// override works as expected?
|
||||
config.DotEnvTryLoad(
|
||||
filepath.Join(util.GetProjectRootDir(), "/internal/config/testdata/.env2.local"),
|
||||
func(k string, v string) error { t.Setenv(k, v); return nil })
|
||||
|
||||
assert.Equal(t, "yes still", os.Getenv("IS_THIS_A_TEST_ENV"))
|
||||
assert.NotEqual(t, "dotenv_override_psql_user", os.Getenv("PSQL_USER"))
|
||||
assert.Equal(t, orgPsqlUser, os.Getenv("PSQL_USER"), "Reset to original does not work!")
|
||||
}
|
||||
|
||||
func TestNoopEnvNotFound(t *testing.T) {
|
||||
assert.NotPanics(t, assert.PanicTestFunc(func() {
|
||||
config.DotEnvTryLoad(
|
||||
filepath.Join(util.GetProjectRootDir(), "/internal/config/testdata/.env.does.not.exist"),
|
||||
func(k string, v string) error { t.Setenv(k, v); return nil },
|
||||
)
|
||||
}), "does not panic on file inexistance")
|
||||
}
|
||||
|
||||
func TestEmptyEnv(t *testing.T) {
|
||||
assert.NotPanics(t, assert.PanicTestFunc(func() {
|
||||
config.DotEnvTryLoad(
|
||||
filepath.Join(util.GetProjectRootDir(), "/internal/config/testdata/.env.local.sample"),
|
||||
func(k string, v string) error { t.Setenv(k, v); return nil },
|
||||
)
|
||||
}), "does not panic on file inexistance")
|
||||
|
||||
assert.Empty(t, os.Getenv("EMPTY_VARIABLE_INIT"), "should be empty")
|
||||
}
|
||||
|
||||
func TestPanicsOnEnvMalform(t *testing.T) {
|
||||
assert.Panics(t, assert.PanicTestFunc(func() {
|
||||
config.DotEnvTryLoad(
|
||||
filepath.Join(util.GetProjectRootDir(), "/internal/config/testdata/.env.local.malformed"),
|
||||
func(k string, v string) error { t.Setenv(k, v); return nil },
|
||||
)
|
||||
}), "does panic on file malform")
|
||||
}
|
||||
19
internal/config/mailer_config.go
Normal file
19
internal/config/mailer_config.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package config
|
||||
|
||||
type MailerTransporter string
|
||||
|
||||
var (
|
||||
MailerTransporterMock MailerTransporter = "mock"
|
||||
MailerTransporterSMTP MailerTransporter = "SMTP"
|
||||
)
|
||||
|
||||
func (m MailerTransporter) String() string {
|
||||
return string(m)
|
||||
}
|
||||
|
||||
type Mailer struct {
|
||||
DefaultSender string
|
||||
Send bool
|
||||
WebTemplatesEmailBaseDirAbs string
|
||||
Transporter string
|
||||
}
|
||||
258
internal/config/server_config.go
Normal file
258
internal/config/server_config.go
Normal file
@@ -0,0 +1,258 @@
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
17
internal/config/server_config_test.go
Normal file
17
internal/config/server_config_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||
)
|
||||
|
||||
func TestPrintServiceEnv(t *testing.T) {
|
||||
config := config.DefaultServiceConfigFromEnv()
|
||||
_, err := json.MarshalIndent(config, "", " ")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
6
internal/config/service_config.go
Normal file
6
internal/config/service_config.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
type PushService struct {
|
||||
UseFCMProvider bool
|
||||
UseMockProvider bool
|
||||
}
|
||||
1
internal/config/testdata/.env.local.malformed
vendored
Normal file
1
internal/config/testdata/.env.local.malformed
vendored
Normal file
@@ -0,0 +1 @@
|
||||
bla bla
|
||||
2
internal/config/testdata/.env.local.sample
vendored
Normal file
2
internal/config/testdata/.env.local.sample
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# empty value should be ok
|
||||
EMPTY_VARIABLE_INIT=
|
||||
3
internal/config/testdata/.env1.local
vendored
Normal file
3
internal/config/testdata/.env1.local
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
IS_THIS_A_TEST_ENV="yes"
|
||||
ORIGINAL_PSQL_USER="${PSQL_USER}"
|
||||
PSQL_USER="dotenv_override_psql_user"
|
||||
2
internal/config/testdata/.env2.local
vendored
Normal file
2
internal/config/testdata/.env2.local
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
IS_THIS_A_TEST_ENV="yes still"
|
||||
PSQL_USER="${ORIGINAL_PSQL_USER}" # reset back to proper env
|
||||
Reference in New Issue
Block a user