refactor: move config related code into dedicated package

This commit is contained in:
Jordan Knott 2021-10-26 22:10:29 -05:00
parent 54553cfbdd
commit 800dd2014c
9 changed files with 159 additions and 103 deletions

View File

@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/utils" "github.com/jordanknott/taskcafe/internal/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -52,6 +53,7 @@ func initConfig() {
viper.SetEnvPrefix("TASKCAFE") viper.SetEnvPrefix("TASKCAFE")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv() viper.AutomaticEnv()
config.InitDefaults()
err := viper.ReadInConfig() err := viper.ReadInConfig()
if err == nil { if err == nil {
@ -61,30 +63,10 @@ func initConfig() {
panic(err) panic(err)
} }
viper.SetDefault("server.hostname", "0.0.0.0:3333")
viper.SetDefault("database.host", "127.0.0.1")
viper.SetDefault("database.name", "taskcafe")
viper.SetDefault("database.user", "taskcafe")
viper.SetDefault("database.password", "taskcafe_test")
viper.SetDefault("queue.broker", "amqp://guest:guest@localhost:5672/")
viper.SetDefault("queue.store", "memcache://localhost:11211")
} }
// Execute the root cobra command // Execute the root cobra command
func Execute() { func Execute() {
viper.SetDefault("server.hostname", "0.0.0.0:3333")
viper.SetDefault("database.host", "127.0.0.1")
viper.SetDefault("database.name", "taskcafe")
viper.SetDefault("database.user", "taskcafe")
viper.SetDefault("database.password", "taskcafe_test")
viper.SetDefault("database.port", "5432")
viper.SetDefault("security.token_expiration", "15m")
viper.SetDefault("queue.broker", "amqp://guest:guest@localhost:5672/")
viper.SetDefault("queue.store", "memcache://localhost:11211")
rootCmd.SetVersionTemplate(VersionTemplate()) rootCmd.SetVersionTemplate(VersionTemplate())
rootCmd.AddCommand(newWebCmd(), newMigrateCmd(), newWorkerCmd(), newResetPasswordCmd(), newSeedCmd()) rootCmd.AddCommand(newWebCmd(), newMigrateCmd(), newWorkerCmd(), newResetPasswordCmd(), newSeedCmd())
rootCmd.Execute() rootCmd.Execute()

View File

@ -1,21 +1,18 @@
package commands package commands
import ( import (
"fmt"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres" "github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source/httpfs" "github.com/golang-migrate/migrate/v4/source/httpfs"
"github.com/google/uuid"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/route" "github.com/jordanknott/taskcafe/internal/route"
"github.com/jordanknott/taskcafe/internal/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -33,13 +30,7 @@ func newWebCmd() *cobra.Command {
log.SetFormatter(Formatter) log.SetFormatter(Formatter)
log.SetLevel(log.InfoLevel) log.SetLevel(log.InfoLevel)
connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s port=%s sslmode=disable", connection := config.GetDatabaseConnectionUri()
viper.GetString("database.user"),
viper.GetString("database.password"),
viper.GetString("database.host"),
viper.GetString("database.name"),
viper.GetString("database.port"),
)
var db *sqlx.DB var db *sqlx.DB
var err error var err error
var retryDuration time.Duration var retryDuration time.Duration
@ -70,36 +61,19 @@ func newWebCmd() *cobra.Command {
} }
} }
secret := viper.GetString("server.secret") appConfig, err := config.GetAppConfig()
if strings.TrimSpace(secret) == "" { if err != nil {
log.Warn("server.secret is not set, generating a random secret") return err
secret = uuid.New().String()
} }
security, err := utils.GetSecurityConfig(viper.GetString("security.token_expiration"), []byte(secret)) r, _ := route.NewRouter(db, appConfig)
r, _ := route.NewRouter(db, utils.EmailConfig{
From: viper.GetString("smtp.from"),
Host: viper.GetString("smtp.host"),
Port: viper.GetInt("smtp.port"),
Username: viper.GetString("smtp.username"),
Password: viper.GetString("smtp.password"),
InsecureSkipVerify: viper.GetBool("smtp.skip_verify"),
}, security)
log.WithFields(log.Fields{"url": viper.GetString("server.hostname")}).Info("starting server") log.WithFields(log.Fields{"url": viper.GetString("server.hostname")}).Info("starting server")
return http.ListenAndServe(viper.GetString("server.hostname"), r) return http.ListenAndServe(viper.GetString("server.hostname"), r)
}, },
} }
viper.SetDefault("smtp.from", "no-reply@example.com")
viper.SetDefault("smtp.host", "localhost")
viper.SetDefault("smtp.port", 587)
viper.SetDefault("smtp.username", "")
viper.SetDefault("smtp.password", "")
viper.SetDefault("smtp.skip_verify", false)
cc.Flags().Bool("migrate", false, "if true, auto run's schema migrations before starting the web server") cc.Flags().Bool("migrate", false, "if true, auto run's schema migrations before starting the web server")
viper.BindPFlag("migrate", cc.Flags().Lookup("migrate")) viper.BindPFlag("migrate", cc.Flags().Lookup("migrate"))
viper.SetDefault("migrate", false) viper.SetDefault("migrate", false)
return cc return cc
} }

129
internal/config/config.go Normal file
View File

@ -0,0 +1,129 @@
package config
import (
"fmt"
"strings"
"time"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
const (
ServerHostname = "server.hostname"
DatabaseHost = "database.host"
DatabaseName = "database.name"
DatabaseUser = "database.user"
DatabasePassword = "database.password"
DatabasePort = "database.port"
DatabaseSslMode = "database.sslmode"
SecurityTokenExpiration = "security.token_expiration"
SecuritySecret = "security.secret"
QueueBroker = "queue.broker"
QueueStore = "queue.store"
SmtpFrom = "smtp.from"
SmtpHost = "smtp.host"
SmtpPort = "smtp.port"
SmtpUsername = "smtp.username"
SmtpPassword = "smtp.password"
SmtpSkipVerify = "false"
)
var defaults = map[string]string{
ServerHostname: "0.0.0.0:3333",
DatabaseHost: "127.0.0.1",
DatabaseName: "taskcafe",
DatabaseUser: "taskcafe",
DatabasePassword: "taskcafe_test",
DatabasePort: "5432",
DatabaseSslMode: "disable",
SecurityTokenExpiration: "15m",
SecuritySecret: "",
QueueBroker: "amqp://guest:guest@localhost:5672/",
QueueStore: "memcache://localhost:11211",
SmtpFrom: "no-reply@example.com",
SmtpHost: "localhost",
SmtpPort: "587",
SmtpUsername: "",
SmtpPassword: "",
SmtpSkipVerify: "false",
}
func InitDefaults() {
for key, value := range defaults {
viper.SetDefault(key, value)
}
}
func GetDatabaseConnectionUri() string {
connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s port=%s sslmode=%s",
viper.GetString(DatabaseUser),
viper.GetString(DatabasePassword),
viper.GetString(DatabaseHost),
viper.GetString(DatabaseName),
viper.GetString(DatabasePort),
viper.GetString(DatabaseSslMode),
)
return connection
}
type AppConfig struct {
Email EmailConfig
Security SecurityConfig
}
type EmailConfig struct {
Host string
Port int
From string
Username string
Password string
SiteURL string
InsecureSkipVerify bool
}
type SecurityConfig struct {
AccessTokenExpiration time.Duration
Secret []byte
}
func GetAppConfig() (AppConfig, error) {
secret := viper.GetString(SecuritySecret)
if strings.TrimSpace(secret) == "" {
log.Warn("server.secret is not set, generating a random secret")
secret = uuid.New().String()
}
securityCfg, err := GetSecurityConfig(viper.GetString(SecurityTokenExpiration), []byte(secret))
if err != nil {
return AppConfig{}, err
}
emailCfg := GetEmailConfig()
return AppConfig{
Email: emailCfg,
Security: securityCfg,
}, nil
}
func GetSecurityConfig(accessTokenExp string, secret []byte) (SecurityConfig, error) {
exp, err := time.ParseDuration(accessTokenExp)
if err != nil {
log.WithError(err).Error("issue parsing duration")
return SecurityConfig{}, err
}
return SecurityConfig{AccessTokenExpiration: exp, Secret: secret}, nil
}
func GetEmailConfig() EmailConfig {
return EmailConfig{
From: viper.GetString("smtp.from"),
Host: viper.GetString("smtp.host"),
Port: viper.GetInt("smtp.port"),
Username: viper.GetString("smtp.username"),
Password: viper.GetString("smtp.password"),
InsecureSkipVerify: viper.GetBool("smtp.skip_verify"),
}
}

View File

@ -17,6 +17,7 @@ import (
"github.com/99designs/gqlgen/graphql/handler/transport" "github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground" "github.com/99designs/gqlgen/graphql/playground"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/db" "github.com/jordanknott/taskcafe/internal/db"
"github.com/jordanknott/taskcafe/internal/logger" "github.com/jordanknott/taskcafe/internal/logger"
"github.com/jordanknott/taskcafe/internal/utils" "github.com/jordanknott/taskcafe/internal/utils"
@ -25,11 +26,11 @@ import (
) )
// NewHandler returns a new graphql endpoint handler. // NewHandler returns a new graphql endpoint handler.
func NewHandler(repo db.Repository, emailConfig utils.EmailConfig) http.Handler { func NewHandler(repo db.Repository, appConfig config.AppConfig) http.Handler {
c := Config{ c := Config{
Resolvers: &Resolver{ Resolvers: &Resolver{
Repository: repo, Repository: repo,
EmailConfig: emailConfig, AppConfig: appConfig,
}, },
} }
c.Directives.HasRole = func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []RoleLevel, level ActionLevel, typeArg ObjectType) (interface{}, error) { c.Directives.HasRole = func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []RoleLevel, level ActionLevel, typeArg ObjectType) (interface{}, error) {

View File

@ -130,7 +130,7 @@ func (r *mutationResolver) InviteProjectMembers(ctx context.Context, input Invit
return &InviteProjectMembersPayload{Ok: false}, err return &InviteProjectMembersPayload{Ok: false}, err
} }
invite := utils.EmailInvite{To: *invitedMember.Email, FullName: *invitedMember.Email, ConfirmToken: confirmToken.ConfirmTokenID.String()} invite := utils.EmailInvite{To: *invitedMember.Email, FullName: *invitedMember.Email, ConfirmToken: confirmToken.ConfirmTokenID.String()}
err = utils.SendEmailInvite(r.EmailConfig, invite) err = utils.SendEmailInvite(r.AppConfig.Email, invite)
if err != nil { if err != nil {
logger.New(ctx).WithError(err).Error("issue sending email") logger.New(ctx).WithError(err).Error("issue sending email")
return &InviteProjectMembersPayload{Ok: false}, err return &InviteProjectMembersPayload{Ok: false}, err

View File

@ -6,13 +6,13 @@ package graph
import ( import (
"sync" "sync"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/db" "github.com/jordanknott/taskcafe/internal/db"
"github.com/jordanknott/taskcafe/internal/utils"
) )
// Resolver handles resolving GraphQL queries & mutations // Resolver handles resolving GraphQL queries & mutations
type Resolver struct { type Resolver struct {
Repository db.Repository Repository db.Repository
EmailConfig utils.EmailConfig AppConfig config.AppConfig
mu sync.Mutex mu sync.Mutex
} }

View File

@ -13,11 +13,11 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/db" "github.com/jordanknott/taskcafe/internal/db"
"github.com/jordanknott/taskcafe/internal/frontend" "github.com/jordanknott/taskcafe/internal/frontend"
"github.com/jordanknott/taskcafe/internal/graph" "github.com/jordanknott/taskcafe/internal/graph"
"github.com/jordanknott/taskcafe/internal/logger" "github.com/jordanknott/taskcafe/internal/logger"
"github.com/jordanknott/taskcafe/internal/utils"
) )
// FrontendHandler serves an embed React client through chi // FrontendHandler serves an embed React client through chi
@ -62,11 +62,11 @@ func (h FrontendHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// TaskcafeHandler contains all the route handlers // TaskcafeHandler contains all the route handlers
type TaskcafeHandler struct { type TaskcafeHandler struct {
repo db.Repository repo db.Repository
SecurityConfig utils.SecurityConfig AppConfig config.AppConfig
} }
// NewRouter creates a new router for chi // NewRouter creates a new router for chi
func NewRouter(dbConnection *sqlx.DB, emailConfig utils.EmailConfig, securityConfig utils.SecurityConfig) (chi.Router, error) { func NewRouter(dbConnection *sqlx.DB, appConfig config.AppConfig) (chi.Router, error) {
formatter := new(log.TextFormatter) formatter := new(log.TextFormatter)
formatter.TimestampFormat = "02-01-2006 15:04:05" formatter.TimestampFormat = "02-01-2006 15:04:05"
formatter.FullTimestamp = true formatter.FullTimestamp = true
@ -93,7 +93,7 @@ func NewRouter(dbConnection *sqlx.DB, emailConfig utils.EmailConfig, securityCon
})) }))
repository := db.NewRepository(dbConnection) repository := db.NewRepository(dbConnection)
taskcafeHandler := TaskcafeHandler{*repository, securityConfig} taskcafeHandler := TaskcafeHandler{*repository, appConfig}
var imgServer = http.FileServer(http.Dir("./uploads/")) var imgServer = http.FileServer(http.Dir("./uploads/"))
r.Group(func(mux chi.Router) { r.Group(func(mux chi.Router) {
@ -109,7 +109,7 @@ func NewRouter(dbConnection *sqlx.DB, emailConfig utils.EmailConfig, securityCon
r.Group(func(mux chi.Router) { r.Group(func(mux chi.Router) {
mux.Use(auth.Middleware) mux.Use(auth.Middleware)
mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload) mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
mux.Handle("/graphql", graph.NewHandler(*repository, emailConfig)) mux.Handle("/graphql", graph.NewHandler(*repository, appConfig))
}) })
frontend := FrontendHandler{staticPath: "build", indexPath: "index.html"} frontend := FrontendHandler{staticPath: "build", indexPath: "index.html"}

View File

@ -3,31 +3,22 @@ package utils
import ( import (
"crypto/tls" "crypto/tls"
"github.com/jordanknott/taskcafe/internal/config"
hermes "github.com/matcornic/hermes/v2" hermes "github.com/matcornic/hermes/v2"
gomail "gopkg.in/mail.v2" gomail "gopkg.in/mail.v2"
) )
type EmailConfig struct {
Host string
Port int
From string
Username string
Password string
SiteURL string
InsecureSkipVerify bool
}
type EmailInvite struct { type EmailInvite struct {
ConfirmToken string ConfirmToken string
FullName string FullName string
To string To string
} }
func SendEmailInvite(config EmailConfig, invite EmailInvite) error { func SendEmailInvite(cfg config.EmailConfig, invite EmailInvite) error {
h := hermes.Hermes{ h := hermes.Hermes{
Product: hermes.Product{ Product: hermes.Product{
Name: "Taskscafe", Name: "Taskscafe",
Link: config.SiteURL, Link: cfg.SiteURL,
Logo: "https://github.com/JordanKnott/taskcafe/raw/master/.github/taskcafe-full.png", Logo: "https://github.com/JordanKnott/taskcafe/raw/master/.github/taskcafe-full.png",
}, },
} }
@ -45,7 +36,7 @@ func SendEmailInvite(config EmailConfig, invite EmailInvite) error {
Color: "#7367F0", // Optional action button color Color: "#7367F0", // Optional action button color
TextColor: "#FFFFFF", TextColor: "#FFFFFF",
Text: "Register your account", Text: "Register your account",
Link: config.SiteURL + "/register?confirmToken=" + invite.ConfirmToken, Link: cfg.SiteURL + "/register?confirmToken=" + invite.ConfirmToken,
}, },
}, },
}, },
@ -67,7 +58,7 @@ func SendEmailInvite(config EmailConfig, invite EmailInvite) error {
m := gomail.NewMessage() m := gomail.NewMessage()
// Set E-Mail sender // Set E-Mail sender
m.SetHeader("From", config.From) m.SetHeader("From", cfg.From)
// Set E-Mail receivers // Set E-Mail receivers
m.SetHeader("To", invite.To) m.SetHeader("To", invite.To)
@ -80,11 +71,11 @@ func SendEmailInvite(config EmailConfig, invite EmailInvite) error {
m.AddAlternative("text/plain", emailBodyPlain) m.AddAlternative("text/plain", emailBodyPlain)
// Settings for SMTP server // Settings for SMTP server
d := gomail.NewDialer(config.Host, config.Port, config.Username, config.Password) d := gomail.NewDialer(cfg.Host, cfg.Port, cfg.Username, cfg.Password)
// This is only needed when SSL/TLS certificate is not valid on server. // This is only needed when SSL/TLS certificate is not valid on server.
// In production this should be set to false. // In production this should be set to false.
d.TLSConfig = &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify} d.TLSConfig = &tls.Config{InsecureSkipVerify: cfg.InsecureSkipVerify}
// Now send E-Mail // Now send E-Mail
if err := d.DialAndSend(m); err != nil { if err := d.DialAndSend(m); err != nil {

View File

@ -1,21 +0,0 @@
package utils
import (
"time"
log "github.com/sirupsen/logrus"
)
type SecurityConfig struct {
AccessTokenExpiration time.Duration
Secret []byte
}
func GetSecurityConfig(accessTokenExp string, secret []byte) (SecurityConfig, error) {
exp, err := time.ParseDuration(accessTokenExp)
if err != nil {
log.WithError(err).Error("issue parsing duration")
return SecurityConfig{}, err
}
return SecurityConfig{AccessTokenExpiration: exp, Secret: secret}, nil
}