1
0
mirror of https://github.com/muety/wakapi.git synced 2023-08-10 21:12:56 +03:00
wakapi/main.go

283 lines
8.2 KiB
Go
Raw Normal View History

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"
"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)
}
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)
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)
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()
}
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{
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
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())
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)
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
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}
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}
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{
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
router.PathPrefix("/api").Handler(negroni.Classic().
2019-11-08 01:11:19 +03:00
With(corsMiddleware).
With(
2019-11-08 01:11:19 +03:00
negroni.HandlerFunc(authenticateMiddleware.Handle),
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
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
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) {
pw := md5.Sum([]byte(cfg.DefaultUserPassword))
2019-05-22 00:09:47 +03:00
pwString := hex.EncodeToString(pw[:])
apiKey := uuid.NewV4().String()
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 {
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
}
}