feat: set html base path from server base path

refactor: services
This commit is contained in:
Ferdinand Mütsch 2020-05-24 17:32:26 +02:00
parent d6e9f0295a
commit c171d31f30
16 changed files with 192 additions and 160 deletions

131
main.go
View File

@ -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

View File

@ -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,

View File

@ -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,
}
}

5
models/models.go Normal file
View File

@ -0,0 +1,5 @@
package models
func init() {
SetConfig(readConfig())
}

View File

@ -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,
}
}

View File

@ -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,
}
}

View File

@ -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)

View File

@ -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(),
}
}

View File

@ -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() {

View File

@ -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

View File

@ -1,5 +0,0 @@
package services
type Initializable interface {
Init()
}

View File

@ -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{}

View File

@ -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

View File

@ -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{}

View File

@ -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
}

View File

@ -1,6 +1,6 @@
<head>
<title>Wakapi Coding Statistics</title>
<base href="/">
<base href="{{ getBasePath }}">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>
<link rel="icon" data-emoji="📊" type="image/png">
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">