2019-05-05 23:36:49 +03:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2019-05-22 00:09:47 +03:00
|
|
|
"crypto/md5"
|
|
|
|
"encoding/hex"
|
2020-03-31 12:24:44 +03:00
|
|
|
"encoding/json"
|
|
|
|
"io/ioutil"
|
2019-05-06 01:58:01 +03:00
|
|
|
"log"
|
2019-05-05 23:36:49 +03:00
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"strconv"
|
2020-03-31 12:24:44 +03:00
|
|
|
"strings"
|
2019-05-05 23:36:49 +03:00
|
|
|
"time"
|
|
|
|
|
2019-05-06 00:23:54 +03:00
|
|
|
"github.com/codegangsta/negroni"
|
2020-04-20 02:58:54 +03:00
|
|
|
"github.com/gobuffalo/packr/v2"
|
2019-05-06 00:23:54 +03:00
|
|
|
"github.com/gorilla/mux"
|
2019-05-11 18:49:56 +03:00
|
|
|
"github.com/jinzhu/gorm"
|
2019-05-06 01:58:01 +03:00
|
|
|
"github.com/joho/godotenv"
|
2019-05-20 21:44:53 +03:00
|
|
|
"github.com/rs/cors"
|
2020-04-20 02:58:54 +03:00
|
|
|
"github.com/rubenv/sql-migrate"
|
2019-05-22 00:09:47 +03:00
|
|
|
uuid "github.com/satori/go.uuid"
|
2019-05-16 23:51:11 +03:00
|
|
|
ini "gopkg.in/ini.v1"
|
2019-05-06 00:23:54 +03:00
|
|
|
|
2020-03-31 13:22:17 +03:00
|
|
|
"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"
|
2019-05-11 18:49:56 +03:00
|
|
|
|
|
|
|
_ "github.com/jinzhu/gorm/dialects/mysql"
|
2020-03-31 13:03:49 +03:00
|
|
|
_ "github.com/jinzhu/gorm/dialects/postgres"
|
2020-03-31 15:43:15 +03:00
|
|
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
2019-05-05 23:36:49 +03:00
|
|
|
)
|
|
|
|
|
2020-03-31 12:24:44 +03:00
|
|
|
// TODO: Refactor entire project to be structured after business domains
|
|
|
|
|
2019-05-16 23:53:03 +03:00
|
|
|
func readConfig() *models.Config {
|
2019-05-11 18:57:58 +03:00
|
|
|
if err := godotenv.Load(); err != nil {
|
2019-05-06 01:58:01 +03:00
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2020-04-26 14:55:19 +03:00
|
|
|
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")
|
2020-04-26 15:00:53 +03:00
|
|
|
defaultUserName := utils.LookupFatal("WAKAPI_DEFAULT_USER_NAME")
|
|
|
|
defaultUserPassword := utils.LookupFatal("WAKAPI_DEFAULT_USER_PASSWORD")
|
2019-11-08 00:56:06 +03:00
|
|
|
dbPort, err := strconv.Atoi(dbPortStr)
|
2019-05-06 01:47:38 +03:00
|
|
|
|
2019-05-16 23:51:11 +03:00
|
|
|
cfg, err := ini.Load("config.ini")
|
|
|
|
if err != nil {
|
2019-10-11 10:06:34 +03:00
|
|
|
log.Fatalf("Fail to read file: %v", err)
|
2019-05-16 23:51:11 +03:00
|
|
|
}
|
|
|
|
|
2020-03-31 13:03:49 +03:00
|
|
|
if dbType == "" {
|
|
|
|
dbType = "mysql"
|
|
|
|
}
|
|
|
|
|
2019-10-11 10:06:34 +03:00
|
|
|
dbMaxConn := cfg.Section("database").Key("max_connections").MustUint(1)
|
2019-05-21 15:02:04 +03:00
|
|
|
addr := cfg.Section("server").Key("listen").MustString("127.0.0.1")
|
2019-05-19 20:49:27 +03:00
|
|
|
port, err := strconv.Atoi(os.Getenv("PORT"))
|
|
|
|
if err != nil {
|
|
|
|
port = cfg.Section("server").Key("port").MustInt()
|
|
|
|
}
|
2019-05-16 23:51:11 +03:00
|
|
|
|
2020-03-09 19:30:23 +03:00
|
|
|
cleanUp := cfg.Section("app").Key("cleanup").MustBool(false)
|
|
|
|
|
2019-05-21 18:16:46 +03:00
|
|
|
// Read custom languages
|
|
|
|
customLangs := make(map[string]string)
|
|
|
|
languageKeys := cfg.Section("languages").Keys()
|
|
|
|
for _, k := range languageKeys {
|
|
|
|
customLangs[k.Name()] = k.MustString("unknown")
|
|
|
|
}
|
|
|
|
|
2020-03-31 12:24:44 +03:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2019-05-16 23:53:03 +03:00
|
|
|
return &models.Config{
|
2020-04-26 15:00:53 +03:00
|
|
|
Env: env,
|
|
|
|
Port: port,
|
|
|
|
Addr: addr,
|
|
|
|
DbHost: dbHost,
|
|
|
|
DbPort: uint(dbPort),
|
|
|
|
DbUser: dbUser,
|
|
|
|
DbPassword: dbPassword,
|
|
|
|
DbName: dbName,
|
|
|
|
DbDialect: dbType,
|
|
|
|
DbMaxConn: dbMaxConn,
|
|
|
|
CleanUp: cleanUp,
|
|
|
|
DefaultUserName: defaultUserName,
|
|
|
|
DefaultUserPassword: defaultUserPassword,
|
|
|
|
CustomLanguages: customLangs,
|
|
|
|
LanguageColors: colors,
|
2019-05-05 23:36:49 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
// Read Config
|
2019-05-06 01:47:38 +03:00
|
|
|
config := readConfig()
|
2020-04-20 02:58:54 +03:00
|
|
|
// Enable line numbers in logging
|
2020-04-25 23:53:55 +03:00
|
|
|
if config.IsDev() {
|
|
|
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
|
|
|
}
|
2019-05-05 23:36:49 +03:00
|
|
|
|
2019-05-11 18:49:56 +03:00
|
|
|
// Connect to database
|
2019-05-16 23:53:03 +03:00
|
|
|
db, err := gorm.Open(config.DbDialect, utils.MakeConnectionString(config))
|
2020-04-20 02:58:54 +03:00
|
|
|
if config.DbDialect == "sqlite3" {
|
|
|
|
db.DB().Exec("PRAGMA foreign_keys = ON;")
|
|
|
|
}
|
2020-03-09 19:30:23 +03:00
|
|
|
db.LogMode(config.IsDev())
|
2019-10-11 11:03:18 +03:00
|
|
|
db.DB().SetMaxIdleConns(int(config.DbMaxConn))
|
|
|
|
db.DB().SetMaxOpenConns(int(config.DbMaxConn))
|
2019-05-05 23:36:49 +03:00
|
|
|
if err != nil {
|
2020-04-20 02:58:54 +03:00
|
|
|
log.Println(err)
|
2019-10-11 11:03:18 +03:00
|
|
|
log.Fatal("Could not connect to database.")
|
2019-05-05 23:36:49 +03:00
|
|
|
}
|
2020-03-31 13:03:49 +03:00
|
|
|
// TODO: Graceful shutdown
|
2019-05-11 18:49:56 +03:00
|
|
|
defer db.Close()
|
|
|
|
|
|
|
|
// Migrate database schema
|
2020-04-25 23:53:55 +03:00
|
|
|
migrateDo := databaseMigrateActions(config.DbDialect)
|
|
|
|
migrateDo(db)
|
2019-05-05 23:36:49 +03:00
|
|
|
|
2019-05-22 00:09:47 +03:00
|
|
|
// Custom migrations and initial data
|
|
|
|
addDefaultUser(db, config)
|
2019-05-21 23:01:14 +03:00
|
|
|
migrateLanguages(db, config)
|
|
|
|
|
2019-05-06 01:40:41 +03:00
|
|
|
// Services
|
2019-11-08 01:11:19 +03:00
|
|
|
aliasSrvc := &services.AliasService{Config: config, Db: db}
|
|
|
|
heartbeatSrvc := &services.HeartbeatService{Config: config, Db: db}
|
|
|
|
userSrvc := &services.UserService{Config: config, Db: db}
|
|
|
|
summarySrvc := &services.SummaryService{Config: config, Db: db, HeartbeatService: heartbeatSrvc, AliasService: aliasSrvc}
|
|
|
|
aggregationSrvc := &services.AggregationService{Config: config, Db: db, UserService: userSrvc, SummaryService: summarySrvc, HeartbeatService: heartbeatSrvc}
|
2019-10-10 00:26:28 +03:00
|
|
|
|
2020-02-20 16:28:55 +03:00
|
|
|
services := []services.Initializable{aliasSrvc, heartbeatSrvc, summarySrvc, userSrvc, aggregationSrvc}
|
|
|
|
for _, s := range services {
|
|
|
|
s.Init()
|
|
|
|
}
|
|
|
|
|
2019-10-11 10:06:34 +03:00
|
|
|
// Aggregate heartbeats to summaries and persist them
|
|
|
|
go aggregationSrvc.Schedule()
|
2019-05-06 01:40:41 +03:00
|
|
|
|
2020-03-09 19:30:23 +03:00
|
|
|
if config.CleanUp {
|
|
|
|
go heartbeatSrvc.ScheduleCleanUp()
|
|
|
|
}
|
|
|
|
|
2019-05-06 01:40:41 +03:00
|
|
|
// Handlers
|
|
|
|
heartbeatHandler := &routes.HeartbeatHandler{HeartbeatSrvc: heartbeatSrvc}
|
2019-05-19 20:49:27 +03:00
|
|
|
summaryHandler := &routes.SummaryHandler{SummarySrvc: summarySrvc}
|
2020-04-08 22:29:11 +03:00
|
|
|
healthHandler := &routes.HealthHandler{Db: db}
|
2019-05-06 01:40:41 +03:00
|
|
|
|
|
|
|
// Middlewares
|
2020-04-08 22:29:11 +03:00
|
|
|
authenticateMiddleware := &middlewares.AuthenticateMiddleware{
|
|
|
|
UserSrvc: userSrvc,
|
|
|
|
WhitelistPaths: []string{"/api/health"},
|
|
|
|
}
|
2020-02-20 16:28:55 +03:00
|
|
|
basicAuthMiddleware := &middlewares.RequireBasicAuthMiddleware{}
|
2019-11-08 01:11:19 +03:00
|
|
|
corsMiddleware := cors.New(cors.Options{
|
2019-05-20 21:44:53 +03:00
|
|
|
AllowedOrigins: []string{"*"},
|
|
|
|
AllowedHeaders: []string{"*"},
|
|
|
|
Debug: false,
|
|
|
|
})
|
2019-05-05 23:36:49 +03:00
|
|
|
|
2019-05-06 00:23:54 +03:00
|
|
|
// Setup Routing
|
|
|
|
router := mux.NewRouter()
|
2020-02-21 14:41:29 +03:00
|
|
|
mainRouter := mux.NewRouter().PathPrefix("/").Subrouter()
|
2019-05-06 00:23:54 +03:00
|
|
|
apiRouter := mux.NewRouter().PathPrefix("/api").Subrouter()
|
|
|
|
|
2020-02-20 16:28:55 +03:00
|
|
|
// Main Routes
|
2020-02-21 14:41:29 +03:00
|
|
|
mainRouter.Path("/").Methods(http.MethodGet).HandlerFunc(summaryHandler.Index)
|
2020-02-20 16:28:55 +03:00
|
|
|
|
2019-05-06 00:23:54 +03:00
|
|
|
// API Routes
|
2020-03-31 12:24:44 +03:00
|
|
|
apiRouter.Path("/heartbeat").Methods(http.MethodPost).HandlerFunc(heartbeatHandler.ApiPost)
|
|
|
|
apiRouter.Path("/summary").Methods(http.MethodGet).HandlerFunc(summaryHandler.ApiGet)
|
2020-04-08 22:29:11 +03:00
|
|
|
apiRouter.Path("/health").Methods(http.MethodGet).HandlerFunc(healthHandler.ApiGet)
|
2019-05-06 00:23:54 +03:00
|
|
|
|
2020-02-20 16:28:55 +03:00
|
|
|
// Static Routes
|
|
|
|
router.PathPrefix("/assets").Handler(negroni.Classic().With(negroni.Wrap(http.FileServer(http.Dir("./static")))))
|
|
|
|
|
2019-05-06 00:23:54 +03:00
|
|
|
// Sub-Routes Setup
|
2019-05-20 21:44:53 +03:00
|
|
|
router.PathPrefix("/api").Handler(negroni.Classic().
|
2019-11-08 01:11:19 +03:00
|
|
|
With(corsMiddleware).
|
2019-05-20 21:44:53 +03:00
|
|
|
With(
|
2019-11-08 01:11:19 +03:00
|
|
|
negroni.HandlerFunc(authenticateMiddleware.Handle),
|
2019-05-20 21:44:53 +03:00
|
|
|
negroni.Wrap(apiRouter),
|
|
|
|
))
|
2019-05-05 23:36:49 +03:00
|
|
|
|
2020-02-21 14:41:29 +03:00
|
|
|
router.PathPrefix("/").Handler(negroni.Classic().With(
|
|
|
|
negroni.HandlerFunc(basicAuthMiddleware.Handle),
|
|
|
|
negroni.HandlerFunc(authenticateMiddleware.Handle),
|
|
|
|
negroni.Wrap(mainRouter),
|
|
|
|
))
|
|
|
|
|
2019-05-05 23:36:49 +03:00
|
|
|
// Listen HTTP
|
2019-05-21 15:02:04 +03:00
|
|
|
portString := config.Addr + ":" + strconv.Itoa(config.Port)
|
2019-05-05 23:36:49 +03:00
|
|
|
s := &http.Server{
|
2019-05-06 00:23:54 +03:00
|
|
|
Handler: router,
|
2019-05-05 23:36:49 +03:00
|
|
|
Addr: portString,
|
|
|
|
ReadTimeout: 10 * time.Second,
|
|
|
|
WriteTimeout: 10 * time.Second,
|
|
|
|
}
|
2019-05-06 01:58:01 +03:00
|
|
|
log.Printf("Listening on %+s\n", portString)
|
2019-05-05 23:36:49 +03:00
|
|
|
s.ListenAndServe()
|
|
|
|
}
|
2019-05-21 23:01:14 +03:00
|
|
|
|
2020-04-25 23:53:55 +03:00
|
|
|
func databaseMigrateActions(dbDialect string) func(db *gorm.DB) {
|
|
|
|
var migrateDo func(db *gorm.DB)
|
|
|
|
if dbDialect == "sqlite3" {
|
|
|
|
migrations := &migrate.PackrMigrationSource{
|
|
|
|
Box: packr.New("migrations", "./migrations/sqlite3"),
|
|
|
|
}
|
|
|
|
migrateDo = func(db *gorm.DB) {
|
|
|
|
n, err := migrate.Exec(db.DB(), "sqlite3", migrations, migrate.Up)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
log.Printf("Applied %d migrations!\n", n)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
migrateDo = func(db *gorm.DB) {
|
|
|
|
db.AutoMigrate(&models.Alias{})
|
|
|
|
db.AutoMigrate(&models.Summary{})
|
|
|
|
db.AutoMigrate(&models.SummaryItem{})
|
|
|
|
db.AutoMigrate(&models.User{})
|
|
|
|
db.AutoMigrate(&models.Heartbeat{}).AddForeignKey("user_id", "users(id)", "RESTRICT", "RESTRICT")
|
|
|
|
db.AutoMigrate(&models.SummaryItem{}).AddForeignKey("summary_id", "summaries(id)", "CASCADE", "CASCADE")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return migrateDo
|
|
|
|
}
|
|
|
|
|
2019-05-21 23:01:14 +03:00
|
|
|
func migrateLanguages(db *gorm.DB, cfg *models.Config) {
|
|
|
|
for k, v := range cfg.CustomLanguages {
|
|
|
|
result := db.Model(models.Heartbeat{}).
|
|
|
|
Where("language = ?", "").
|
|
|
|
Where("entity LIKE ?", "%."+k).
|
|
|
|
Updates(models.Heartbeat{Language: v})
|
|
|
|
if result.Error != nil {
|
|
|
|
log.Fatal(result.Error)
|
|
|
|
}
|
2019-05-22 00:09:47 +03:00
|
|
|
if result.RowsAffected > 0 {
|
|
|
|
log.Printf("Migrated %+v rows for custom language %+s.\n", result.RowsAffected, k)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func addDefaultUser(db *gorm.DB, cfg *models.Config) {
|
2020-04-26 15:00:53 +03:00
|
|
|
pw := md5.Sum([]byte(cfg.DefaultUserPassword))
|
2019-05-22 00:09:47 +03:00
|
|
|
pwString := hex.EncodeToString(pw[:])
|
2019-11-07 14:56:05 +03:00
|
|
|
apiKey := uuid.NewV4().String()
|
2020-04-26 15:00:53 +03:00
|
|
|
u := &models.User{ID: cfg.DefaultUserName, Password: pwString, ApiKey: apiKey}
|
2019-05-22 00:09:47 +03:00
|
|
|
result := db.FirstOrCreate(u, &models.User{ID: u.ID})
|
|
|
|
if result.Error != nil {
|
|
|
|
log.Println("Unable to create default user.")
|
|
|
|
log.Fatal(result.Error)
|
|
|
|
}
|
|
|
|
if result.RowsAffected > 0 {
|
2020-04-26 15:00:53 +03:00
|
|
|
log.Printf("Created default user '%s' with password '%s' and API key '%s'.\n", u.ID, cfg.DefaultUserPassword, u.ApiKey)
|
2019-05-21 23:01:14 +03:00
|
|
|
}
|
|
|
|
}
|