wakapi/main.go

384 lines
13 KiB
Go
Raw Normal View History

2019-05-05 23:36:49 +03:00
package main
import (
"embed"
"io/fs"
"log"
"net"
"net/http"
"os"
"strconv"
"time"
"github.com/emvi/logbuch"
"github.com/gorilla/handlers"
2019-05-06 00:23:54 +03:00
"github.com/gorilla/mux"
2022-08-19 14:48:03 +03:00
"github.com/lpar/gzipped/v2"
conf "github.com/muety/wakapi/config"
2020-03-31 13:22:17 +03:00
"github.com/muety/wakapi/middlewares"
2022-08-19 14:48:03 +03:00
"github.com/muety/wakapi/migrations"
"github.com/muety/wakapi/repositories"
2020-03-31 13:22:17 +03:00
"github.com/muety/wakapi/routes"
2022-08-19 14:48:03 +03:00
"github.com/muety/wakapi/routes/api"
shieldsV1Routes "github.com/muety/wakapi/routes/compat/shields/v1"
wtV1Routes "github.com/muety/wakapi/routes/compat/wakatime/v1"
2022-08-19 14:48:03 +03:00
"github.com/muety/wakapi/routes/relay"
2020-03-31 13:22:17 +03:00
"github.com/muety/wakapi/services"
2022-08-19 14:48:03 +03:00
"github.com/muety/wakapi/services/mail"
fsutils "github.com/muety/wakapi/utils/fs"
_ "gorm.io/driver/mysql"
_ "gorm.io/driver/postgres"
_ "gorm.io/driver/sqlite"
"gorm.io/gorm"
2022-08-19 14:48:03 +03:00
"gorm.io/gorm/logger"
2019-05-05 23:36:49 +03:00
)
// Embed version.txt
//go:embed version.txt
var version string
// Embed static files
//go:embed static
var staticFiles embed.FS
2020-05-24 17:34:32 +03:00
var (
db *gorm.DB
config *conf.Config
2020-05-24 17:34:32 +03:00
)
var (
aliasRepository repositories.IAliasRepository
heartbeatRepository repositories.IHeartbeatRepository
userRepository repositories.IUserRepository
languageMappingRepository repositories.ILanguageMappingRepository
projectLabelRepository repositories.IProjectLabelRepository
summaryRepository repositories.ISummaryRepository
keyValueRepository repositories.IKeyValueRepository
diagnosticsRepository repositories.IDiagnosticsRepository
2022-03-18 20:20:13 +03:00
metricsRepository *repositories.MetricsRepository
)
2020-05-24 17:34:32 +03:00
var (
aliasService services.IAliasService
heartbeatService services.IHeartbeatService
userService services.IUserService
languageMappingService services.ILanguageMappingService
projectLabelService services.IProjectLabelService
durationService services.IDurationService
summaryService services.ISummaryService
aggregationService services.IAggregationService
2021-04-05 23:57:57 +03:00
mailService services.IMailService
keyValueService services.IKeyValueService
2021-04-30 15:07:14 +03:00
reportService services.IReportService
diagnosticsService services.IDiagnosticsService
miscService services.IMiscService
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
2021-02-07 13:54:07 +03:00
// @title Wakapi API
// @version 1.0
// @description REST API to interact with [Wakapi](https://wakapi.dev)
// @description
// @description ## Authentication
// @description Set header `Authorization` to your API Key encoded as Base64 and prefixed with `Basic`
// @description **Example:** `Basic ODY2NDhkNzQtMTljNS00NTJiLWJhMDEtZmIzZWM3MGQ0YzJmCg==`
// @contact.name Ferdinand Mütsch
// @contact.url https://github.com/muety
// @contact.email ferdinand@muetsch.io
// @license.name GPL-3.0
// @license.url https://github.com/muety/wakapi/blob/master/LICENSE
// @securitydefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization
// @BasePath /api
2019-05-05 23:36:49 +03:00
func main() {
config = conf.Load(version)
2020-05-24 17:34:32 +03:00
// Set log level
if config.IsDev() {
logbuch.SetLevel(logbuch.LevelDebug)
} else {
logbuch.SetLevel(logbuch.LevelInfo)
}
2019-05-05 23:36:49 +03:00
2021-01-31 20:29:24 +03:00
// Set up GORM
gormLogger := logger.New(
log.New(os.Stdout, "", log.LstdFlags),
logger.Config{
SlowThreshold: time.Minute,
Colorful: false,
LogLevel: logger.Silent,
},
)
2019-05-11 18:49:56 +03:00
// Connect to database
2020-05-24 17:34:32 +03:00
var err error
2021-01-31 20:29:24 +03:00
db, err = gorm.Open(config.Db.GetDialector(), &gorm.Config{Logger: gormLogger})
2022-08-19 14:48:03 +03:00
if err != nil {
logbuch.Error(err.Error())
logbuch.Fatal("could not open database")
}
if config.Db.IsSQLite() {
db.Exec("PRAGMA foreign_keys = ON;")
2020-04-20 02:58:54 +03:00
}
if config.IsDev() {
db = db.Debug()
}
sqlDb, err := db.DB()
sqlDb.SetMaxIdleConns(int(config.Db.MaxConn))
sqlDb.SetMaxOpenConns(int(config.Db.MaxConn))
2019-05-05 23:36:49 +03:00
if err != nil {
logbuch.Error(err.Error())
logbuch.Fatal("could not connect to database")
2019-05-05 23:36:49 +03:00
}
defer sqlDb.Close()
2019-05-11 18:49:56 +03:00
// Migrate database schema
if !config.SkipMigrations {
migrations.Run(db, config)
}
2019-05-05 23:36:49 +03:00
// Repositories
aliasRepository = repositories.NewAliasRepository(db)
heartbeatRepository = repositories.NewHeartbeatRepository(db)
userRepository = repositories.NewUserRepository(db)
languageMappingRepository = repositories.NewLanguageMappingRepository(db)
projectLabelRepository = repositories.NewProjectLabelRepository(db)
summaryRepository = repositories.NewSummaryRepository(db)
keyValueRepository = repositories.NewKeyValueRepository(db)
diagnosticsRepository = repositories.NewDiagnosticsRepository(db)
2022-03-18 20:20:13 +03:00
metricsRepository = repositories.NewMetricsRepository(db)
2019-05-06 01:40:41 +03:00
// Services
mailService = mail.NewMailService()
aliasService = services.NewAliasService(aliasRepository)
userService = services.NewUserService(mailService, userRepository)
languageMappingService = services.NewLanguageMappingService(languageMappingRepository)
projectLabelService = services.NewProjectLabelService(projectLabelRepository)
heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService)
durationService = services.NewDurationService(heartbeatService)
summaryService = services.NewSummaryService(summaryRepository, durationService, aliasService, projectLabelService)
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService)
keyValueService = services.NewKeyValueService(keyValueRepository)
2021-04-30 15:07:14 +03:00
reportService = services.NewReportService(summaryService, userService, mailService)
diagnosticsService = services.NewDiagnosticsService(diagnosticsRepository)
miscService = services.NewMiscService(userService, summaryService, keyValueService)
2020-02-20 16:28:55 +03:00
// Schedule background tasks
go aggregationService.Schedule()
go miscService.ScheduleCountTotalTime()
go reportService.Schedule()
2019-05-06 01:40:41 +03:00
2020-11-08 14:46:12 +03:00
routes.Init()
// API Handlers
healthApiHandler := api.NewHealthApiHandler(db)
heartbeatApiHandler := api.NewHeartbeatApiHandler(userService, heartbeatService, languageMappingService)
summaryApiHandler := api.NewSummaryApiHandler(userService, summaryService)
2022-03-18 20:20:13 +03:00
metricsHandler := api.NewMetricsHandler(userService, summaryService, heartbeatService, keyValueService, metricsRepository)
diagnosticsHandler := api.NewDiagnosticsApiHandler(userService, diagnosticsService)
avatarHandler := api.NewAvatarHandler()
badgeHandler := api.NewBadgeHandler(userService, summaryService)
// Compat Handlers
2021-09-06 20:01:49 +03:00
wakatimeV1StatusBarHandler := wtV1Routes.NewStatusBarHandler(userService, summaryService)
wakatimeV1AllHandler := wtV1Routes.NewAllTimeHandler(userService, summaryService)
wakatimeV1SummariesHandler := wtV1Routes.NewSummariesHandler(userService, summaryService)
wakatimeV1StatsHandler := wtV1Routes.NewStatsHandler(userService, summaryService)
wakatimeV1UsersHandler := wtV1Routes.NewUsersHandler(userService, heartbeatService)
wakatimeV1ProjectsHandler := wtV1Routes.NewProjectsHandler(userService, heartbeatService)
wakatimeV1HeartbeatsHandler := wtV1Routes.NewHeartbeatHandler(userService, heartbeatService)
shieldV1BadgeHandler := shieldsV1Routes.NewBadgeHandler(summaryService, userService)
// MVC Handlers
summaryHandler := routes.NewSummaryHandler(summaryService, userService)
2021-06-12 11:44:19 +03:00
settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, summaryService, aliasService, aggregationService, languageMappingService, projectLabelService, keyValueService, mailService)
homeHandler := routes.NewHomeHandler(keyValueService)
2021-04-05 23:57:57 +03:00
loginHandler := routes.NewLoginHandler(userService, mailService)
2020-11-06 23:19:54 +03:00
imprintHandler := routes.NewImprintHandler(keyValueService)
2019-05-06 01:40:41 +03:00
// Other Handlers
relayHandler := relay.NewRelayHandler()
// Setup Routers
2019-05-06 00:23:54 +03:00
router := mux.NewRouter()
rootRouter := router.PathPrefix("/").Subrouter()
apiRouter := router.PathPrefix("/api").Subrouter().StrictSlash(true)
2021-06-24 22:40:51 +03:00
// https://github.com/gorilla/mux/issues/416
router.NotFoundHandler = router.NewRoute().BuildOnly().HandlerFunc(http.NotFound).GetHandler()
router.NotFoundHandler = middlewares.NewLoggingMiddleware(logbuch.Info, []string{
"/assets",
"/favicon",
"/service-worker.js",
})(router.NotFoundHandler)
// Globally used middlewares
router.Use(middlewares.NewPrincipalMiddleware())
router.Use(middlewares.NewLoggingMiddleware(logbuch.Info, []string{"/assets", "/api/health"}))
router.Use(handlers.RecoveryHandler())
if config.Sentry.Dsn != "" {
router.Use(middlewares.NewSentryMiddleware())
}
rootRouter.Use(middlewares.NewSecurityMiddleware())
// Route registrations
homeHandler.RegisterRoutes(rootRouter)
loginHandler.RegisterRoutes(rootRouter)
imprintHandler.RegisterRoutes(rootRouter)
summaryHandler.RegisterRoutes(rootRouter)
settingsHandler.RegisterRoutes(rootRouter)
relayHandler.RegisterRoutes(rootRouter)
// API route registrations
summaryApiHandler.RegisterRoutes(apiRouter)
healthApiHandler.RegisterRoutes(apiRouter)
heartbeatApiHandler.RegisterRoutes(apiRouter)
metricsHandler.RegisterRoutes(apiRouter)
diagnosticsHandler.RegisterRoutes(apiRouter)
avatarHandler.RegisterRoutes(apiRouter)
badgeHandler.RegisterRoutes(apiRouter)
2021-09-06 20:01:49 +03:00
wakatimeV1StatusBarHandler.RegisterRoutes(apiRouter)
wakatimeV1AllHandler.RegisterRoutes(apiRouter)
wakatimeV1SummariesHandler.RegisterRoutes(apiRouter)
wakatimeV1StatsHandler.RegisterRoutes(apiRouter)
wakatimeV1UsersHandler.RegisterRoutes(apiRouter)
wakatimeV1ProjectsHandler.RegisterRoutes(apiRouter)
wakatimeV1HeartbeatsHandler.RegisterRoutes(apiRouter)
shieldV1BadgeHandler.RegisterRoutes(apiRouter)
2020-02-20 16:28:55 +03:00
// Static Routes
// https://github.com/golang/go/issues/43431
embeddedStatic, _ := fs.Sub(staticFiles, "static")
static := conf.ChooseFS("static", embeddedStatic)
assetsFileServer := gzipped.FileServer(fsutils.NewExistsHttpFS(
fsutils.NewExistsFS(static).WithCache(!config.IsDev()),
))
staticFileServer := http.FileServer(http.FS(
fsutils.NeuteredFileSystem{FS: static},
2021-12-20 18:41:50 +03:00
))
router.PathPrefix("/contribute.json").Handler(staticFileServer)
router.PathPrefix("/assets").Handler(assetsFileServer)
router.PathPrefix("/swagger-ui").Handler(staticFileServer)
router.PathPrefix("/docs").Handler(
middlewares.NewFileTypeFilterMiddleware([]string{".go"})(staticFileServer),
)
2020-02-21 14:41:29 +03:00
2019-05-05 23:36:49 +03:00
// Listen HTTP
listen(router)
}
func listen(handler http.Handler) {
var s4, s6, sSocket *http.Server
// IPv4
if config.Server.ListenIpV4 != "" {
bindString4 := config.Server.ListenIpV4 + ":" + strconv.Itoa(config.Server.Port)
s4 = &http.Server{
Handler: handler,
Addr: bindString4,
2021-06-24 22:56:47 +03:00
ReadTimeout: time.Duration(config.Server.TimeoutSec) * time.Second,
WriteTimeout: time.Duration(config.Server.TimeoutSec) * time.Second,
}
}
// IPv6
if config.Server.ListenIpV6 != "" {
bindString6 := "[" + config.Server.ListenIpV6 + "]:" + strconv.Itoa(config.Server.Port)
s6 = &http.Server{
Handler: handler,
Addr: bindString6,
2021-06-24 22:56:47 +03:00
ReadTimeout: time.Duration(config.Server.TimeoutSec) * time.Second,
WriteTimeout: time.Duration(config.Server.TimeoutSec) * time.Second,
}
2019-05-05 23:36:49 +03:00
}
// UNIX domain socket
if config.Server.ListenSocket != "" {
// Remove if exists
if _, err := os.Stat(config.Server.ListenSocket); err == nil {
logbuch.Info("--> Removing unix socket %s", config.Server.ListenSocket)
if err := os.Remove(config.Server.ListenSocket); err != nil {
logbuch.Fatal(err.Error())
}
}
sSocket = &http.Server{
2021-06-24 22:40:51 +03:00
Handler: handler,
2021-06-24 22:56:47 +03:00
ReadTimeout: time.Duration(config.Server.TimeoutSec) * time.Second,
WriteTimeout: time.Duration(config.Server.TimeoutSec) * time.Second,
}
}
if config.UseTLS() {
if s4 != nil {
logbuch.Info("--> Listening for HTTPS on %s... ✅", s4.Addr)
2021-01-05 13:28:51 +03:00
go func() {
if err := s4.ListenAndServeTLS(config.Server.TlsCertPath, config.Server.TlsKeyPath); err != nil {
logbuch.Fatal(err.Error())
2021-01-05 13:28:51 +03:00
}
}()
}
if s6 != nil {
logbuch.Info("--> Listening for HTTPS on %s... ✅", s6.Addr)
2021-01-05 13:28:51 +03:00
go func() {
if err := s6.ListenAndServeTLS(config.Server.TlsCertPath, config.Server.TlsKeyPath); err != nil {
logbuch.Fatal(err.Error())
2021-01-05 13:28:51 +03:00
}
}()
}
if sSocket != nil {
logbuch.Info("--> Listening for HTTPS on %s... ✅", config.Server.ListenSocket)
go func() {
unixListener, err := net.Listen("unix", config.Server.ListenSocket)
if err != nil {
logbuch.Fatal(err.Error())
}
if err := sSocket.ServeTLS(unixListener, config.Server.TlsCertPath, config.Server.TlsKeyPath); err != nil {
logbuch.Fatal(err.Error())
}
}()
}
} else {
if s4 != nil {
logbuch.Info("--> Listening for HTTP on %s... ✅", s4.Addr)
2021-01-05 13:28:51 +03:00
go func() {
if err := s4.ListenAndServe(); err != nil {
logbuch.Fatal(err.Error())
2021-01-05 13:28:51 +03:00
}
}()
}
if s6 != nil {
logbuch.Info("--> Listening for HTTP on %s... ✅", s6.Addr)
2021-01-05 13:28:51 +03:00
go func() {
if err := s6.ListenAndServe(); err != nil {
logbuch.Fatal(err.Error())
2021-01-05 13:28:51 +03:00
}
}()
}
if sSocket != nil {
logbuch.Info("--> Listening for HTTP on %s... ✅", config.Server.ListenSocket)
go func() {
unixListener, err := net.Listen("unix", config.Server.ListenSocket)
if err != nil {
logbuch.Fatal(err.Error())
}
if err := sSocket.Serve(unixListener); err != nil {
logbuch.Fatal(err.Error())
}
}()
}
}
<-make(chan interface{}, 1)
2019-05-05 23:36:49 +03:00
}