diff --git a/main.go b/main.go index 6d7d1c3..288dc7c 100644 --- a/main.go +++ b/main.go @@ -1,29 +1,21 @@ package main import ( - "encoding/json" "github.com/gorilla/handlers" - "github.com/gorilla/securecookie" - "io/ioutil" "log" "net/http" - "os" "strconv" - "strings" "time" "github.com/gobuffalo/packr/v2" "github.com/gorilla/mux" "github.com/jinzhu/gorm" - "github.com/joho/godotenv" - "github.com/rubenv/sql-migrate" - ini "gopkg.in/ini.v1" - "github.com/muety/wakapi/middlewares" "github.com/muety/wakapi/models" "github.com/muety/wakapi/routes" "github.com/muety/wakapi/services" "github.com/muety/wakapi/utils" + "github.com/rubenv/sql-migrate" _ "github.com/jinzhu/gorm/dialects/mysql" _ "github.com/jinzhu/gorm/dialects/postgres" @@ -45,103 +37,8 @@ var ( // TODO: Refactor entire project to be structured after business domains -func readConfig() *models.Config { - if err := godotenv.Load(); err != nil { - log.Fatal(err) - } - - env := utils.LookupFatal("ENV") - dbType := utils.LookupFatal("WAKAPI_DB_TYPE") - dbUser := utils.LookupFatal("WAKAPI_DB_USER") - dbPassword := utils.LookupFatal("WAKAPI_DB_PASSWORD") - dbHost := utils.LookupFatal("WAKAPI_DB_HOST") - dbName := utils.LookupFatal("WAKAPI_DB_NAME") - dbPortStr := utils.LookupFatal("WAKAPI_DB_PORT") - defaultUserName := utils.LookupFatal("WAKAPI_DEFAULT_USER_NAME") - defaultUserPassword := utils.LookupFatal("WAKAPI_DEFAULT_USER_PASSWORD") - dbPort, err := strconv.Atoi(dbPortStr) - - cfg, err := ini.Load("config.ini") - if err != nil { - log.Fatalf("Fail to read file: %v", err) - } - - if dbType == "" { - dbType = "mysql" - } - - dbMaxConn := cfg.Section("database").Key("max_connections").MustUint(1) - addr := cfg.Section("server").Key("listen").MustString("127.0.0.1") - port, err := strconv.Atoi(os.Getenv("PORT")) - if err != nil { - port = cfg.Section("server").Key("port").MustInt() - } - - basePath := cfg.Section("server").Key("base_path").MustString("/") - if strings.HasSuffix(basePath, "/") { - basePath = basePath[:len(basePath)-1] - } - - cleanUp := cfg.Section("app").Key("cleanup").MustBool(false) - - // Read custom languages - customLangs := make(map[string]string) - languageKeys := cfg.Section("languages").Keys() - for _, k := range languageKeys { - customLangs[k.Name()] = k.MustString("unknown") - } - - // Read language colors - // Source: https://raw.githubusercontent.com/ozh/github-colors/master/colors.json - var colors = make(map[string]string) - var rawColors map[string]struct { - Color string `json:"color"` - Url string `json:"url"` - } - - data, err := ioutil.ReadFile("data/colors.json") - if err != nil { - log.Fatal(err) - } - - if err := json.Unmarshal(data, &rawColors); err != nil { - log.Fatal(err) - } - - for k, v := range rawColors { - colors[strings.ToLower(k)] = v.Color - } - - // TODO: Read keys from env, so that users are not logged out every time the server is restarted - secureCookie := securecookie.New( - securecookie.GenerateRandomKey(64), - securecookie.GenerateRandomKey(32), - ) - - return &models.Config{ - Env: env, - Port: port, - Addr: addr, - BasePath: basePath, - DbHost: dbHost, - DbPort: uint(dbPort), - DbUser: dbUser, - DbPassword: dbPassword, - DbName: dbName, - DbDialect: dbType, - DbMaxConn: dbMaxConn, - CleanUp: cleanUp, - SecureCookie: secureCookie, - DefaultUserName: defaultUserName, - DefaultUserPassword: defaultUserPassword, - CustomLanguages: customLangs, - LanguageColors: colors, - } -} - func main() { - // Read Config - config = readConfig() + config = models.GetConfig() // Enable line numbers in logging if config.IsDev() { @@ -159,7 +56,7 @@ func main() { db.DB().SetMaxOpenConns(int(config.DbMaxConn)) if err != nil { log.Println(err) - log.Fatal("Could not connect to database.") + log.Fatal("could not connect to database") } // TODO: Graceful shutdown defer db.Close() @@ -169,16 +66,11 @@ func main() { migrateDo(db) // Services - aliasService = &services.AliasService{Config: config, Db: db} - heartbeatService = &services.HeartbeatService{Config: config, Db: db} - userService = &services.UserService{Config: config, Db: db} - summaryService = &services.SummaryService{Config: config, Db: db, HeartbeatService: heartbeatService, AliasService: aliasService} - aggregationService = &services.AggregationService{Config: config, Db: db, UserService: userService, SummaryService: summaryService, HeartbeatService: heartbeatService} - - svcs := []services.Initializable{aliasService, heartbeatService, userService, summaryService, aggregationService} - for _, s := range svcs { - s.Init() - } + aliasService = services.NewAliasService(db) + heartbeatService = services.NewHeartbeatService(db) + userService = services.NewUserService(db) + summaryService = services.NewSummaryService(db, heartbeatService, aliasService) + aggregationService = services.NewAggregationService(db, userService, summaryService, heartbeatService) // Custom migrations and initial data addDefaultUser() @@ -192,10 +84,10 @@ func main() { } // Handlers - heartbeatHandler := routes.NewHeartbeatHandler(config, heartbeatService) - summaryHandler := routes.NewSummaryHandler(config, summaryService) + heartbeatHandler := routes.NewHeartbeatHandler(heartbeatService) + summaryHandler := routes.NewSummaryHandler(summaryService) healthHandler := routes.NewHealthHandler(db) - indexHandler := routes.NewIndexHandler(config, userService) + indexHandler := routes.NewIndexHandler(userService) // Setup Routers router := mux.NewRouter() @@ -208,7 +100,6 @@ func main() { loggingMiddleware := middlewares.NewLoggingMiddleware().Handler corsMiddleware := handlers.CORS() authenticateMiddleware := middlewares.NewAuthenticateMiddleware( - config, userService, []string{"/api/health"}, ).Handler diff --git a/middlewares/authenticate.go b/middlewares/authenticate.go index 6010095..4b7f3ab 100644 --- a/middlewares/authenticate.go +++ b/middlewares/authenticate.go @@ -21,9 +21,9 @@ type AuthenticateMiddleware struct { whitelistPaths []string } -func NewAuthenticateMiddleware(config *models.Config, userService *services.UserService, whitelistPaths []string) *AuthenticateMiddleware { +func NewAuthenticateMiddleware(userService *services.UserService, whitelistPaths []string) *AuthenticateMiddleware { return &AuthenticateMiddleware{ - config: config, + config: models.GetConfig(), userSrvc: userService, cache: cache.New(1*time.Hour, 2*time.Hour), whitelistPaths: whitelistPaths, diff --git a/models/config.go b/models/config.go index 750173b..245cc92 100644 --- a/models/config.go +++ b/models/config.go @@ -1,6 +1,18 @@ package models -import "github.com/gorilla/securecookie" +import ( + "encoding/json" + "github.com/gorilla/securecookie" + "github.com/joho/godotenv" + "gopkg.in/ini.v1" + "io/ioutil" + "log" + "os" + "strconv" + "strings" +) + +var cfg *Config type Config struct { Env string @@ -27,3 +39,113 @@ type Config struct { func (c *Config) IsDev() bool { return c.Env == "dev" } + +func SetConfig(config *Config) { + cfg = config +} + +func GetConfig() *Config { + return cfg +} + +func LookupFatal(key string) string { + v, ok := os.LookupEnv(key) + if !ok { + log.Fatalf("missing env variable '%s'", key) + } + return v +} + +func readConfig() *Config { + if err := godotenv.Load(); err != nil { + log.Fatal(err) + } + + env := LookupFatal("ENV") + dbType := LookupFatal("WAKAPI_DB_TYPE") + dbUser := LookupFatal("WAKAPI_DB_USER") + dbPassword := LookupFatal("WAKAPI_DB_PASSWORD") + dbHost := LookupFatal("WAKAPI_DB_HOST") + dbName := LookupFatal("WAKAPI_DB_NAME") + dbPortStr := LookupFatal("WAKAPI_DB_PORT") + defaultUserName := LookupFatal("WAKAPI_DEFAULT_USER_NAME") + defaultUserPassword := LookupFatal("WAKAPI_DEFAULT_USER_PASSWORD") + dbPort, err := strconv.Atoi(dbPortStr) + + cfg, err := ini.Load("config.ini") + if err != nil { + log.Fatalf("Fail to read file: %v", err) + } + + if dbType == "" { + dbType = "mysql" + } + + dbMaxConn := cfg.Section("database").Key("max_connections").MustUint(1) + addr := cfg.Section("server").Key("listen").MustString("127.0.0.1") + port, err := strconv.Atoi(os.Getenv("PORT")) + if err != nil { + port = cfg.Section("server").Key("port").MustInt() + } + + basePath := cfg.Section("server").Key("base_path").MustString("/") + if strings.HasSuffix(basePath, "/") { + basePath = basePath[:len(basePath)-1] + } + + cleanUp := cfg.Section("app").Key("cleanup").MustBool(false) + + // Read custom languages + customLangs := make(map[string]string) + languageKeys := cfg.Section("languages").Keys() + for _, k := range languageKeys { + customLangs[k.Name()] = k.MustString("unknown") + } + + // Read language colors + // Source: https://raw.githubusercontent.com/ozh/github-colors/master/colors.json + var colors = make(map[string]string) + var rawColors map[string]struct { + Color string `json:"color"` + Url string `json:"url"` + } + + data, err := ioutil.ReadFile("data/colors.json") + if err != nil { + log.Fatal(err) + } + + if err := json.Unmarshal(data, &rawColors); err != nil { + log.Fatal(err) + } + + for k, v := range rawColors { + colors[strings.ToLower(k)] = v.Color + } + + // TODO: Read keys from env, so that users are not logged out every time the server is restarted + secureCookie := securecookie.New( + securecookie.GenerateRandomKey(64), + securecookie.GenerateRandomKey(32), + ) + + return &Config{ + Env: env, + Port: port, + Addr: addr, + BasePath: basePath, + DbHost: dbHost, + DbPort: uint(dbPort), + DbUser: dbUser, + DbPassword: dbPassword, + DbName: dbName, + DbDialect: dbType, + DbMaxConn: dbMaxConn, + CleanUp: cleanUp, + SecureCookie: secureCookie, + DefaultUserName: defaultUserName, + DefaultUserPassword: defaultUserPassword, + CustomLanguages: customLangs, + LanguageColors: colors, + } +} diff --git a/models/models.go b/models/models.go new file mode 100644 index 0000000..97c58f4 --- /dev/null +++ b/models/models.go @@ -0,0 +1,5 @@ +package models + +func init() { + SetConfig(readConfig()) +} diff --git a/routes/heartbeat.go b/routes/heartbeat.go index c52e82c..4fe965d 100644 --- a/routes/heartbeat.go +++ b/routes/heartbeat.go @@ -16,10 +16,10 @@ type HeartbeatHandler struct { heartbeatSrvc *services.HeartbeatService } -func NewHeartbeatHandler(config *models.Config, heartbearService *services.HeartbeatService) *HeartbeatHandler { +func NewHeartbeatHandler(heartbeatService *services.HeartbeatService) *HeartbeatHandler { return &HeartbeatHandler{ - config: config, - heartbeatSrvc: heartbearService, + config: models.GetConfig(), + heartbeatSrvc: heartbeatService, } } diff --git a/routes/index.go b/routes/index.go index 1698149..d1fdc8f 100644 --- a/routes/index.go +++ b/routes/index.go @@ -18,9 +18,9 @@ type IndexHandler struct { var loginDecoder = schema.NewDecoder() var signupDecoder = schema.NewDecoder() -func NewIndexHandler(config *models.Config, userService *services.UserService) *IndexHandler { +func NewIndexHandler(userService *services.UserService) *IndexHandler { return &IndexHandler{ - config: config, + config: models.GetConfig(), userSrvc: userService, } } diff --git a/routes/routes.go b/routes/routes.go index ed1e79d..dbd9468 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -2,6 +2,7 @@ package routes import ( "fmt" + "github.com/muety/wakapi/models" "github.com/muety/wakapi/utils" "html/template" "io/ioutil" @@ -22,6 +23,9 @@ func loadTemplates() { "date": utils.FormatDateHuman, "title": strings.Title, "capitalize": utils.Capitalize, + "getBasePath": func() string { + return models.GetConfig().BasePath + }, }) templates = make(map[string]*template.Template) diff --git a/routes/summary.go b/routes/summary.go index 21f7f15..a6075b2 100644 --- a/routes/summary.go +++ b/routes/summary.go @@ -24,10 +24,10 @@ type SummaryHandler struct { config *models.Config } -func NewSummaryHandler(config *models.Config, summaryService *services.SummaryService) *SummaryHandler { +func NewSummaryHandler(summaryService *services.SummaryService) *SummaryHandler { return &SummaryHandler{ cummarySrvc: summaryService, - config: config, + config: models.GetConfig(), } } diff --git a/services/aggregation.go b/services/aggregation.go index 078e024..181ed55 100644 --- a/services/aggregation.go +++ b/services/aggregation.go @@ -22,14 +22,22 @@ type AggregationService struct { HeartbeatService *HeartbeatService } +func NewAggregationService(db *gorm.DB, userService *UserService, summaryService *SummaryService, heartbeatService *HeartbeatService) *AggregationService { + return &AggregationService{ + Config: models.GetConfig(), + Db: db, + UserService: userService, + SummaryService: summaryService, + HeartbeatService: heartbeatService, + } +} + type AggregationJob struct { UserID string From time.Time To time.Time } -func (srv *AggregationService) Init() {} - // Schedule a job to (re-)generate summaries every day shortly after midnight // TODO: Make configurable func (srv *AggregationService) Schedule() { diff --git a/services/alias.go b/services/alias.go index b3332a1..5744ad0 100644 --- a/services/alias.go +++ b/services/alias.go @@ -13,9 +13,14 @@ type AliasService struct { Db *gorm.DB } -var userAliases sync.Map +func NewAliasService(db *gorm.DB) *AliasService { + return &AliasService{ + Config: models.GetConfig(), + Db: db, + } +} -func (srv *AliasService) Init() {} +var userAliases sync.Map func (srv *AliasService) LoadUserAliases(userId string) error { var aliases []*models.Alias diff --git a/services/common.go b/services/common.go deleted file mode 100644 index 436ee6f..0000000 --- a/services/common.go +++ /dev/null @@ -1,5 +0,0 @@ -package services - -type Initializable interface { - Init() -} diff --git a/services/heartbeat.go b/services/heartbeat.go index ddbbf28..d4f5ee6 100644 --- a/services/heartbeat.go +++ b/services/heartbeat.go @@ -21,7 +21,12 @@ type HeartbeatService struct { Db *gorm.DB } -func (srv *HeartbeatService) Init() {} +func NewHeartbeatService(db *gorm.DB) *HeartbeatService { + return &HeartbeatService{ + Config: models.GetConfig(), + Db: db, + } +} func (srv *HeartbeatService) InsertBatch(heartbeats []*models.Heartbeat) error { var batch []interface{} diff --git a/services/summary.go b/services/summary.go index b66a711..ced2a89 100644 --- a/services/summary.go +++ b/services/summary.go @@ -21,15 +21,21 @@ type SummaryService struct { AliasService *AliasService } +func NewSummaryService(db *gorm.DB, heartbeatService *HeartbeatService, aliasService *AliasService) *SummaryService { + return &SummaryService{ + Config: models.GetConfig(), + Cache: cache.New(24*time.Hour, 24*time.Hour), + Db: db, + HeartbeatService: heartbeatService, + AliasService: aliasService, + } +} + type Interval struct { Start time.Time End time.Time } -func (srv *SummaryService) Init() { - srv.Cache = cache.New(24*time.Hour, 24*time.Hour) -} - func (srv *SummaryService) Construct(from, to time.Time, user *models.User, recompute bool) (*models.Summary, error) { var existingSummaries []*models.Summary var cacheKey string diff --git a/services/user.go b/services/user.go index 4970ef8..d94b17f 100644 --- a/services/user.go +++ b/services/user.go @@ -13,7 +13,12 @@ type UserService struct { Db *gorm.DB } -func (srv *UserService) Init() {} +func NewUserService(db *gorm.DB) *UserService { + return &UserService{ + Config: models.GetConfig(), + Db: db, + } +} func (srv *UserService) GetUserById(userId string) (*models.User, error) { u := &models.User{} diff --git a/utils/environment.go b/utils/environment.go deleted file mode 100644 index b225228..0000000 --- a/utils/environment.go +++ /dev/null @@ -1,14 +0,0 @@ -package utils - -import ( - "log" - "os" -) - -func LookupFatal(key string) string { - v, ok := os.LookupEnv(key) - if !ok { - log.Fatalf("missing env variable '%s'", key) - } - return v -} diff --git a/views/head.tpl.html b/views/head.tpl.html index 58920ba..7054e2a 100644 --- a/views/head.tpl.html +++ b/views/head.tpl.html @@ -1,6 +1,6 @@