diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 1068bc3..546be68 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -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() diff --git a/internal/commands/web.go b/internal/commands/web.go index 020184d..d535bcd 100644 --- a/internal/commands/web.go +++ b/internal/commands/web.go @@ -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 } diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..0f70bd8 --- /dev/null +++ b/internal/config/config.go @@ -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"), + } +} diff --git a/internal/graph/graph.go b/internal/graph/graph.go index 8664c09..95e9479 100644 --- a/internal/graph/graph.go +++ b/internal/graph/graph.go @@ -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, + Repository: repo, + AppConfig: appConfig, }, } c.Directives.HasRole = func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []RoleLevel, level ActionLevel, typeArg ObjectType) (interface{}, error) { diff --git a/internal/graph/project.resolvers.go b/internal/graph/project.resolvers.go index e01826a..ab1abf1 100644 --- a/internal/graph/project.resolvers.go +++ b/internal/graph/project.resolvers.go @@ -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 diff --git a/internal/graph/resolver.go b/internal/graph/resolver.go index 16b868c..5f6a7a8 100644 --- a/internal/graph/resolver.go +++ b/internal/graph/resolver.go @@ -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 - mu sync.Mutex + Repository db.Repository + AppConfig config.AppConfig + mu sync.Mutex } diff --git a/internal/route/route.go b/internal/route/route.go index e785a92..f2243e7 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -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 @@ -61,12 +61,12 @@ 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 + repo db.Repository + 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"} diff --git a/internal/utils/mail.go b/internal/utils/mail.go index bab8273..40c95f8 100644 --- a/internal/utils/mail.go +++ b/internal/utils/mail.go @@ -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 { diff --git a/internal/utils/security.go b/internal/utils/security.go deleted file mode 100644 index 5ca0604..0000000 --- a/internal/utils/security.go +++ /dev/null @@ -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 -}