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"
"strings"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/utils"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -52,6 +53,7 @@ func initConfig() {
viper.SetEnvPrefix("TASKCAFE")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
config.InitDefaults()
err := viper.ReadInConfig()
if err == nil {
@ -61,30 +63,10 @@ func initConfig() {
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
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.AddCommand(newWebCmd(), newMigrateCmd(), newWorkerCmd(), newResetPasswordCmd(), newSeedCmd())
rootCmd.Execute()

View File

@ -1,21 +1,18 @@
package commands
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source/httpfs"
"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/jmoiron/sqlx"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/route"
"github.com/jordanknott/taskcafe/internal/utils"
log "github.com/sirupsen/logrus"
)
@ -33,13 +30,7 @@ func newWebCmd() *cobra.Command {
log.SetFormatter(Formatter)
log.SetLevel(log.InfoLevel)
connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s port=%s sslmode=disable",
viper.GetString("database.user"),
viper.GetString("database.password"),
viper.GetString("database.host"),
viper.GetString("database.name"),
viper.GetString("database.port"),
)
connection := config.GetDatabaseConnectionUri()
var db *sqlx.DB
var err error
var retryDuration time.Duration
@ -70,36 +61,19 @@ func newWebCmd() *cobra.Command {
}
}
secret := viper.GetString("server.secret")
if strings.TrimSpace(secret) == "" {
log.Warn("server.secret is not set, generating a random secret")
secret = uuid.New().String()
appConfig, err := config.GetAppConfig()
if err != nil {
return err
}
security, err := utils.GetSecurityConfig(viper.GetString("security.token_expiration"), []byte(secret))
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)
r, _ := route.NewRouter(db, appConfig)
log.WithFields(log.Fields{"url": viper.GetString("server.hostname")}).Info("starting server")
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")
viper.BindPFlag("migrate", cc.Flags().Lookup("migrate"))
viper.SetDefault("migrate", false)
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/playground"
"github.com/google/uuid"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/db"
"github.com/jordanknott/taskcafe/internal/logger"
"github.com/jordanknott/taskcafe/internal/utils"
@ -25,11 +26,11 @@ import (
)
// 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{
Resolvers: &Resolver{
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) {

View File

@ -130,7 +130,7 @@ func (r *mutationResolver) InviteProjectMembers(ctx context.Context, input Invit
return &InviteProjectMembersPayload{Ok: false}, err
}
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 {
logger.New(ctx).WithError(err).Error("issue sending email")
return &InviteProjectMembersPayload{Ok: false}, err

View File

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

View File

@ -13,11 +13,11 @@ import (
"os"
"path/filepath"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/db"
"github.com/jordanknott/taskcafe/internal/frontend"
"github.com/jordanknott/taskcafe/internal/graph"
"github.com/jordanknott/taskcafe/internal/logger"
"github.com/jordanknott/taskcafe/internal/utils"
)
// 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
type TaskcafeHandler struct {
repo db.Repository
SecurityConfig utils.SecurityConfig
AppConfig config.AppConfig
}
// 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.TimestampFormat = "02-01-2006 15:04:05"
formatter.FullTimestamp = true
@ -93,7 +93,7 @@ func NewRouter(dbConnection *sqlx.DB, emailConfig utils.EmailConfig, securityCon
}))
repository := db.NewRepository(dbConnection)
taskcafeHandler := TaskcafeHandler{*repository, securityConfig}
taskcafeHandler := TaskcafeHandler{*repository, appConfig}
var imgServer = http.FileServer(http.Dir("./uploads/"))
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) {
mux.Use(auth.Middleware)
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"}

View File

@ -3,31 +3,22 @@ package utils
import (
"crypto/tls"
"github.com/jordanknott/taskcafe/internal/config"
hermes "github.com/matcornic/hermes/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 {
ConfirmToken string
FullName string
To string
}
func SendEmailInvite(config EmailConfig, invite EmailInvite) error {
func SendEmailInvite(cfg config.EmailConfig, invite EmailInvite) error {
h := hermes.Hermes{
Product: hermes.Product{
Name: "Taskscafe",
Link: config.SiteURL,
Link: cfg.SiteURL,
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
TextColor: "#FFFFFF",
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()
// Set E-Mail sender
m.SetHeader("From", config.From)
m.SetHeader("From", cfg.From)
// Set E-Mail receivers
m.SetHeader("To", invite.To)
@ -80,11 +71,11 @@ func SendEmailInvite(config EmailConfig, invite EmailInvite) error {
m.AddAlternative("text/plain", emailBodyPlain)
// 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.
// 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
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
}