2019-05-05 23:36:49 +03:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2020-05-24 15:50:04 +03:00
|
|
|
"github.com/gorilla/handlers"
|
2020-09-29 19:55:07 +03:00
|
|
|
conf "github.com/muety/wakapi/config"
|
2019-05-06 01:58:01 +03:00
|
|
|
"log"
|
2019-05-05 23:36:49 +03:00
|
|
|
"net/http"
|
|
|
|
"strconv"
|
|
|
|
"time"
|
|
|
|
|
2019-05-06 00:23:54 +03:00
|
|
|
"github.com/gorilla/mux"
|
2019-05-11 18:49:56 +03:00
|
|
|
"github.com/jinzhu/gorm"
|
2020-05-30 21:41:27 +03:00
|
|
|
_ "github.com/jinzhu/gorm/dialects/mysql"
|
|
|
|
_ "github.com/jinzhu/gorm/dialects/postgres"
|
|
|
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
2020-03-31 13:22:17 +03:00
|
|
|
"github.com/muety/wakapi/middlewares"
|
|
|
|
"github.com/muety/wakapi/models"
|
|
|
|
"github.com/muety/wakapi/routes"
|
2020-09-12 17:09:23 +03:00
|
|
|
shieldsV1Routes "github.com/muety/wakapi/routes/compat/shields/v1"
|
|
|
|
wtV1Routes "github.com/muety/wakapi/routes/compat/wakatime/v1"
|
2020-03-31 13:22:17 +03:00
|
|
|
"github.com/muety/wakapi/services"
|
|
|
|
"github.com/muety/wakapi/utils"
|
2019-05-05 23:36:49 +03:00
|
|
|
)
|
|
|
|
|
2020-05-24 17:34:32 +03:00
|
|
|
var (
|
|
|
|
db *gorm.DB
|
2020-09-29 19:55:07 +03:00
|
|
|
config *conf.Config
|
2020-05-24 17:34:32 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
aliasService *services.AliasService
|
|
|
|
heartbeatService *services.HeartbeatService
|
|
|
|
userService *services.UserService
|
|
|
|
summaryService *services.SummaryService
|
|
|
|
aggregationService *services.AggregationService
|
2020-05-30 21:41:27 +03:00
|
|
|
keyValueService *services.KeyValueService
|
2020-05-24 17:34:32 +03:00
|
|
|
)
|
|
|
|
|
2020-03-31 12:24:44 +03:00
|
|
|
// TODO: Refactor entire project to be structured after business domains
|
|
|
|
|
2019-05-05 23:36:49 +03:00
|
|
|
func main() {
|
2020-09-29 19:55:07 +03:00
|
|
|
config = conf.Load()
|
2020-05-24 17:34:32 +03:00
|
|
|
|
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
|
|
|
|
2020-08-23 14:30:04 +03:00
|
|
|
// Show data loss warning
|
2020-10-04 11:37:38 +03:00
|
|
|
if config.App.CleanUp {
|
2020-08-23 14:30:04 +03:00
|
|
|
promptAbort("`CLEANUP` is set to `true`, which may cause data loss. Are you sure to continue?", 5)
|
|
|
|
}
|
|
|
|
|
2019-05-11 18:49:56 +03:00
|
|
|
// Connect to database
|
2020-05-24 17:34:32 +03:00
|
|
|
var err error
|
2020-10-04 11:37:38 +03:00
|
|
|
db, err = gorm.Open(config.Db.Dialect, utils.MakeConnectionString(config))
|
|
|
|
if config.Db.Dialect == "sqlite3" {
|
2020-04-20 02:58:54 +03:00
|
|
|
db.DB().Exec("PRAGMA foreign_keys = ON;")
|
|
|
|
}
|
2020-03-09 19:30:23 +03:00
|
|
|
db.LogMode(config.IsDev())
|
2020-10-04 11:37:38 +03:00
|
|
|
db.DB().SetMaxIdleConns(int(config.Db.MaxConn))
|
|
|
|
db.DB().SetMaxOpenConns(int(config.Db.MaxConn))
|
2019-05-05 23:36:49 +03:00
|
|
|
if err != nil {
|
2020-04-20 02:58:54 +03:00
|
|
|
log.Println(err)
|
2020-05-24 18:32:26 +03:00
|
|
|
log.Fatal("could not connect to database")
|
2019-05-05 23:36:49 +03:00
|
|
|
}
|
2019-05-11 18:49:56 +03:00
|
|
|
defer db.Close()
|
|
|
|
|
|
|
|
// Migrate database schema
|
2020-05-30 21:41:27 +03:00
|
|
|
runDatabaseMigrations()
|
|
|
|
applyFixtures()
|
2019-05-05 23:36:49 +03:00
|
|
|
|
2019-05-06 01:40:41 +03:00
|
|
|
// Services
|
2020-05-24 18:32:26 +03:00
|
|
|
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)
|
2020-05-30 21:41:27 +03:00
|
|
|
keyValueService = services.NewKeyValueService(db)
|
2020-02-20 16:28:55 +03:00
|
|
|
|
2020-05-24 17:34:32 +03:00
|
|
|
// Custom migrations and initial data
|
|
|
|
migrateLanguages()
|
|
|
|
|
2019-10-11 10:06:34 +03:00
|
|
|
// Aggregate heartbeats to summaries and persist them
|
2020-05-24 17:34:32 +03:00
|
|
|
go aggregationService.Schedule()
|
2019-05-06 01:40:41 +03:00
|
|
|
|
2020-10-04 11:37:38 +03:00
|
|
|
if config.App.CleanUp {
|
2020-05-24 17:34:32 +03:00
|
|
|
go heartbeatService.ScheduleCleanUp()
|
2020-03-09 19:30:23 +03:00
|
|
|
}
|
|
|
|
|
2020-09-06 13:15:46 +03:00
|
|
|
// TODO: move endpoint registration to the respective routes files
|
|
|
|
|
2019-05-06 01:40:41 +03:00
|
|
|
// Handlers
|
2020-05-24 18:32:26 +03:00
|
|
|
heartbeatHandler := routes.NewHeartbeatHandler(heartbeatService)
|
|
|
|
summaryHandler := routes.NewSummaryHandler(summaryService)
|
2020-05-24 14:41:19 +03:00
|
|
|
healthHandler := routes.NewHealthHandler(db)
|
2020-06-07 20:28:32 +03:00
|
|
|
settingsHandler := routes.NewSettingsHandler(userService)
|
2020-05-30 21:41:27 +03:00
|
|
|
publicHandler := routes.NewIndexHandler(userService, keyValueService)
|
2020-09-12 17:09:23 +03:00
|
|
|
wakatimeV1AllHandler := wtV1Routes.NewAllTimeHandler(summaryService)
|
|
|
|
wakatimeV1SummariesHandler := wtV1Routes.NewSummariesHandler(summaryService)
|
|
|
|
shieldV1BadgeHandler := shieldsV1Routes.NewBadgeHandler(summaryService, userService)
|
2019-05-06 01:40:41 +03:00
|
|
|
|
2020-05-24 15:50:04 +03:00
|
|
|
// Setup Routers
|
2019-05-06 00:23:54 +03:00
|
|
|
router := mux.NewRouter()
|
2020-05-24 22:19:05 +03:00
|
|
|
publicRouter := router.PathPrefix("/").Subrouter()
|
2020-06-07 20:28:32 +03:00
|
|
|
settingsRouter := publicRouter.PathPrefix("/settings").Subrouter()
|
2020-05-24 22:19:05 +03:00
|
|
|
summaryRouter := publicRouter.PathPrefix("/summary").Subrouter()
|
2020-05-24 15:50:04 +03:00
|
|
|
apiRouter := router.PathPrefix("/api").Subrouter()
|
2020-09-12 17:09:23 +03:00
|
|
|
compatRouter := apiRouter.PathPrefix("/compat").Subrouter()
|
|
|
|
wakatimeV1Router := compatRouter.PathPrefix("/wakatime/v1").Subrouter()
|
|
|
|
shieldsV1Router := compatRouter.PathPrefix("/shields/v1").Subrouter()
|
2019-05-06 00:23:54 +03:00
|
|
|
|
2020-05-24 15:50:04 +03:00
|
|
|
// Middlewares
|
|
|
|
recoveryMiddleware := handlers.RecoveryHandler()
|
|
|
|
loggingMiddleware := middlewares.NewLoggingMiddleware().Handler
|
|
|
|
corsMiddleware := handlers.CORS()
|
|
|
|
authenticateMiddleware := middlewares.NewAuthenticateMiddleware(
|
2020-05-24 17:34:32 +03:00
|
|
|
userService,
|
2020-09-12 17:09:23 +03:00
|
|
|
[]string{"/api/health", "/api/compat/shields/v1"},
|
2020-05-24 15:50:04 +03:00
|
|
|
).Handler
|
|
|
|
|
|
|
|
// Router configs
|
|
|
|
router.Use(loggingMiddleware, recoveryMiddleware)
|
|
|
|
summaryRouter.Use(authenticateMiddleware)
|
2020-06-07 20:28:32 +03:00
|
|
|
settingsRouter.Use(authenticateMiddleware)
|
2020-05-24 15:50:04 +03:00
|
|
|
apiRouter.Use(corsMiddleware, authenticateMiddleware)
|
|
|
|
|
|
|
|
// Public Routes
|
2020-06-07 20:28:32 +03:00
|
|
|
publicRouter.Path("/").Methods(http.MethodGet).HandlerFunc(publicHandler.GetIndex)
|
|
|
|
publicRouter.Path("/login").Methods(http.MethodPost).HandlerFunc(publicHandler.PostLogin)
|
|
|
|
publicRouter.Path("/logout").Methods(http.MethodPost).HandlerFunc(publicHandler.PostLogout)
|
|
|
|
publicRouter.Path("/signup").Methods(http.MethodGet).HandlerFunc(publicHandler.GetSignup)
|
|
|
|
publicRouter.Path("/signup").Methods(http.MethodPost).HandlerFunc(publicHandler.PostSignup)
|
|
|
|
publicRouter.Path("/imprint").Methods(http.MethodGet).HandlerFunc(publicHandler.GetImprint)
|
2020-05-24 14:41:19 +03:00
|
|
|
|
|
|
|
// Summary Routes
|
2020-06-07 20:28:32 +03:00
|
|
|
summaryRouter.Methods(http.MethodGet).HandlerFunc(summaryHandler.GetIndex)
|
|
|
|
|
|
|
|
// Settings Routes
|
|
|
|
settingsRouter.Methods(http.MethodGet).HandlerFunc(settingsHandler.GetIndex)
|
|
|
|
settingsRouter.Path("/credentials").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostCredentials)
|
|
|
|
settingsRouter.Path("/reset").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostResetApiKey)
|
2020-09-12 17:09:23 +03:00
|
|
|
settingsRouter.Path("/badges").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostToggleBadges)
|
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-09-12 17:09:23 +03:00
|
|
|
// Wakatime compat V1 API Routes
|
|
|
|
wakatimeV1Router.Path("/users/{user}/all_time_since_today").Methods(http.MethodGet).HandlerFunc(wakatimeV1AllHandler.ApiGet)
|
|
|
|
wakatimeV1Router.Path("/users/{user}/summaries").Methods(http.MethodGet).HandlerFunc(wakatimeV1SummariesHandler.ApiGet)
|
|
|
|
|
|
|
|
// Shields.io compat API Routes
|
|
|
|
shieldsV1Router.PathPrefix("/{user}").Methods(http.MethodGet).HandlerFunc(shieldV1BadgeHandler.ApiGet)
|
2020-09-06 13:15:46 +03:00
|
|
|
|
2020-02-20 16:28:55 +03:00
|
|
|
// Static Routes
|
2020-05-24 15:50:04 +03:00
|
|
|
router.PathPrefix("/assets").Handler(http.FileServer(http.Dir("./static")))
|
2020-02-21 14:41:29 +03:00
|
|
|
|
2019-05-05 23:36:49 +03:00
|
|
|
// Listen HTTP
|
2020-10-04 12:35:23 +03:00
|
|
|
portString := config.Server.ListenIpV4 + ":" + strconv.Itoa(config.Server.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-05-30 21:41:27 +03:00
|
|
|
func runDatabaseMigrations() {
|
2020-10-04 11:37:38 +03:00
|
|
|
if err := config.GetMigrationFunc(config.Db.Dialect)(db); err != nil {
|
2020-05-30 21:41:27 +03:00
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func applyFixtures() {
|
2020-10-04 11:37:38 +03:00
|
|
|
if err := config.GetFixturesFunc(config.Db.Dialect)(db); err != nil {
|
2020-05-30 21:41:27 +03:00
|
|
|
log.Fatal(err)
|
2020-04-25 23:53:55 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-24 17:34:32 +03:00
|
|
|
func migrateLanguages() {
|
2020-10-04 11:37:38 +03:00
|
|
|
for k, v := range config.App.CustomLanguages {
|
2019-05-21 23:01:14 +03:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-08-23 14:30:04 +03:00
|
|
|
|
|
|
|
func promptAbort(message string, timeoutSec int) {
|
|
|
|
log.Printf("[WARNING] %s.\nTo abort server startup, press Ctrl+C.\n", message)
|
|
|
|
for i := timeoutSec; i > 0; i-- {
|
|
|
|
log.Printf("Starting in %d seconds ...\n", i)
|
|
|
|
time.Sleep(1 * time.Second)
|
|
|
|
}
|
|
|
|
}
|