From a2368ff76aeaf448095050739d34b66ce9ca5fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferdinand=20M=C3=BCtsch?= Date: Wed, 3 Feb 2021 21:28:02 +0100 Subject: [PATCH] refactor: significant changes related to routing and general code cleanup --- main.go | 78 +++++++++++--------------- routes/api/health.go | 33 +++++++++++ routes/{ => api}/heartbeat.go | 21 ++++--- routes/api/summary.go | 37 ++++++++++++ routes/compat/shields/v1/badge.go | 9 ++- routes/compat/wakatime/v1/all_time.go | 8 +-- routes/compat/wakatime/v1/summaries.go | 8 +-- routes/handler.go | 1 - routes/health.go | 34 ----------- routes/home.go | 2 - routes/imprint.go | 2 - routes/login.go | 2 - routes/settings.go | 11 ++-- routes/summary.go | 48 ++++------------ routes/utils/summary_utils.go | 27 +++++++++ version.txt | 2 +- 16 files changed, 171 insertions(+), 152 deletions(-) create mode 100644 routes/api/health.go rename routes/{ => api}/heartbeat.go (77%) create mode 100644 routes/api/summary.go delete mode 100644 routes/health.go create mode 100644 routes/utils/summary_utils.go diff --git a/main.go b/main.go index c0c1e2d..2bf1826 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( conf "github.com/muety/wakapi/config" "github.com/muety/wakapi/migrations" "github.com/muety/wakapi/repositories" + "github.com/muety/wakapi/routes/api" "gorm.io/gorm/logger" "log" "net/http" @@ -18,7 +19,6 @@ import ( "github.com/gorilla/mux" "github.com/muety/wakapi/middlewares" - customMiddleware "github.com/muety/wakapi/middlewares/custom" "github.com/muety/wakapi/routes" shieldsV1Routes "github.com/muety/wakapi/routes/compat/shields/v1" wtV1Routes "github.com/muety/wakapi/routes/compat/wakatime/v1" @@ -122,69 +122,57 @@ func main() { go aggregationService.Schedule() go miscService.ScheduleCountTotalTime() - // TODO: move endpoint registration to the respective routes files - routes.Init() - // Handlers - summaryHandler := routes.NewSummaryHandler(summaryService) - healthHandler := routes.NewHealthHandler(db) - heartbeatHandler := routes.NewHeartbeatHandler(heartbeatService, languageMappingService) - settingsHandler := routes.NewSettingsHandler(userService, summaryService, aliasService, aggregationService, languageMappingService) - homeHandler := routes.NewHomeHandler(keyValueService) - loginHandler := routes.NewLoginHandler(userService) - imprintHandler := routes.NewImprintHandler(keyValueService) + // API Handlers + healthApiHandler := api.NewHealthApiHandler(db) + heartbeatApiHandler := api.NewHeartbeatApiHandler(heartbeatService, languageMappingService) + summaryApiHandler := api.NewSummaryApiHandler(summaryService) + + // Compat Handlers wakatimeV1AllHandler := wtV1Routes.NewAllTimeHandler(summaryService) wakatimeV1SummariesHandler := wtV1Routes.NewSummariesHandler(summaryService) shieldV1BadgeHandler := shieldsV1Routes.NewBadgeHandler(summaryService, userService) + // MVC Handlers + summaryHandler := routes.NewSummaryHandler(summaryService, userService) + settingsHandler := routes.NewSettingsHandler(userService, summaryService, aliasService, aggregationService, languageMappingService) + homeHandler := routes.NewHomeHandler(keyValueService) + loginHandler := routes.NewLoginHandler(userService) + imprintHandler := routes.NewImprintHandler(keyValueService) + // Setup Routers router := mux.NewRouter() - publicRouter := router.PathPrefix("/").Subrouter() - settingsRouter := publicRouter.PathPrefix("/settings").Subrouter() - summaryRouter := publicRouter.PathPrefix("/summary").Subrouter() + rootRouter := router.PathPrefix("/").Subrouter() apiRouter := router.PathPrefix("/api").Subrouter() - summaryApiRouter := apiRouter.PathPrefix("/summary").Subrouter() - heartbeatApiRouter := apiRouter.PathPrefix("/heartbeat").Subrouter() - healthApiRouter := apiRouter.PathPrefix("/health").Subrouter() - compatRouter := apiRouter.PathPrefix("/compat").Subrouter() - wakatimeV1Router := compatRouter.PathPrefix("/wakatime/v1/users/{user}").Subrouter() - shieldsV1Router := compatRouter.PathPrefix("/shields/v1/{user}").Subrouter() + compatApiRouter := apiRouter.PathPrefix("/compat").Subrouter() - // Middlewares + // Globally used middlewares recoveryMiddleware := handlers.RecoveryHandler() - loggingMiddleware := middlewares.NewLoggingMiddleware( - // Use logbuch here once https://github.com/emvi/logbuch/issues/4 is realized - log.New(os.Stdout, "", log.LstdFlags), - ) + loggingMiddleware := middlewares.NewLoggingMiddleware(log.New(os.Stdout, "", log.LstdFlags)) corsMiddleware := handlers.CORS() - authenticateMiddleware := middlewares.NewAuthenticateMiddleware( - userService, - []string{"/api/health", "/api/compat/shields/v1"}, - ).Handler - wakatimeRelayMiddleware := customMiddleware.NewWakatimeRelayMiddleware().Handler + authenticateMiddleware := middlewares.NewAuthenticateMiddleware(userService, []string{"/api/health", "/api/compat/shields/v1"}).Handler // Router configs router.Use(loggingMiddleware, recoveryMiddleware) - summaryRouter.Use(authenticateMiddleware) - settingsRouter.Use(authenticateMiddleware) apiRouter.Use(corsMiddleware, authenticateMiddleware) - heartbeatApiRouter.Use(wakatimeRelayMiddleware) // Route registrations - homeHandler.RegisterRoutes(publicRouter) - loginHandler.RegisterRoutes(publicRouter) - imprintHandler.RegisterRoutes(publicRouter) - summaryHandler.RegisterRoutes(summaryRouter) - settingsHandler.RegisterRoutes(settingsRouter) + homeHandler.RegisterRoutes(rootRouter) + loginHandler.RegisterRoutes(rootRouter) + imprintHandler.RegisterRoutes(rootRouter) + summaryHandler.RegisterRoutes(rootRouter) + settingsHandler.RegisterRoutes(rootRouter) - // API Route registrations - summaryHandler.RegisterAPIRoutes(summaryApiRouter) - healthHandler.RegisterAPIRoutes(healthApiRouter) - heartbeatHandler.RegisterAPIRoutes(heartbeatApiRouter) - wakatimeV1AllHandler.RegisterAPIRoutes(wakatimeV1Router) - wakatimeV1SummariesHandler.RegisterAPIRoutes(wakatimeV1Router) - shieldV1BadgeHandler.RegisterAPIRoutes(shieldsV1Router) + // API route registrations + summaryApiHandler.RegisterRoutes(apiRouter) + healthApiHandler.RegisterRoutes(apiRouter) + heartbeatApiHandler.RegisterRoutes(apiRouter) + + // Compat route registrations + wakatimeV1AllHandler.RegisterRoutes(compatApiRouter) + wakatimeV1SummariesHandler.RegisterRoutes(compatApiRouter) + shieldV1BadgeHandler.RegisterRoutes(compatApiRouter) // Static Routes router.PathPrefix("/assets").Handler(http.FileServer(pkger.Dir("/static"))) diff --git a/routes/api/health.go b/routes/api/health.go new file mode 100644 index 0000000..a8165ef --- /dev/null +++ b/routes/api/health.go @@ -0,0 +1,33 @@ +package api + +import ( + "fmt" + "github.com/gorilla/mux" + "gorm.io/gorm" + "net/http" +) + +type HealthApiHandler struct { + db *gorm.DB +} + +func NewHealthApiHandler(db *gorm.DB) *HealthApiHandler { + return &HealthApiHandler{db: db} +} + +func (h *HealthApiHandler) RegisterRoutes(router *mux.Router) { + r := router.PathPrefix("/health").Subrouter() + r.Methods(http.MethodGet).HandlerFunc(h.Get) +} + +func (h *HealthApiHandler) Get(w http.ResponseWriter, r *http.Request) { + var dbStatus int + if sqlDb, err := h.db.DB(); err == nil { + if err := sqlDb.Ping(); err == nil { + dbStatus = 1 + } + } + + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte(fmt.Sprintf("app=1\ndb=%d", dbStatus))) +} diff --git a/routes/heartbeat.go b/routes/api/heartbeat.go similarity index 77% rename from routes/heartbeat.go rename to routes/api/heartbeat.go index 053067f..6e535d0 100644 --- a/routes/heartbeat.go +++ b/routes/api/heartbeat.go @@ -1,10 +1,11 @@ -package routes +package api import ( "encoding/json" "github.com/emvi/logbuch" "github.com/gorilla/mux" conf "github.com/muety/wakapi/config" + customMiddleware "github.com/muety/wakapi/middlewares/custom" "github.com/muety/wakapi/services" "github.com/muety/wakapi/utils" "net/http" @@ -12,14 +13,14 @@ import ( "github.com/muety/wakapi/models" ) -type HeartbeatHandler struct { +type HeartbeatApiHandler struct { config *conf.Config heartbeatSrvc services.IHeartbeatService languageMappingSrvc services.ILanguageMappingService } -func NewHeartbeatHandler(heartbeatService services.IHeartbeatService, languageMappingService services.ILanguageMappingService) *HeartbeatHandler { - return &HeartbeatHandler{ +func NewHeartbeatApiHandler(heartbeatService services.IHeartbeatService, languageMappingService services.ILanguageMappingService) *HeartbeatApiHandler { + return &HeartbeatApiHandler{ config: conf.Get(), heartbeatSrvc: heartbeatService, languageMappingSrvc: languageMappingService, @@ -30,13 +31,15 @@ type heartbeatResponseVm struct { Responses [][]interface{} `json:"responses"` } -func (h *HeartbeatHandler) RegisterRoutes(router *mux.Router) {} - -func (h *HeartbeatHandler) RegisterAPIRoutes(router *mux.Router) { - router.Methods(http.MethodPost).HandlerFunc(h.ApiPost) +func (h *HeartbeatApiHandler) RegisterRoutes(router *mux.Router) { + r := router.PathPrefix("/heartbeat").Subrouter() + r.Use( + customMiddleware.NewWakatimeRelayMiddleware().Handler, + ) + router.Methods(http.MethodPost).HandlerFunc(h.Post) } -func (h *HeartbeatHandler) ApiPost(w http.ResponseWriter, r *http.Request) { +func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) { var heartbeats []*models.Heartbeat user := r.Context().Value(models.UserKey).(*models.User) opSys, editor, _ := utils.ParseUserAgent(r.Header.Get("User-Agent")) diff --git a/routes/api/summary.go b/routes/api/summary.go new file mode 100644 index 0000000..a306b2f --- /dev/null +++ b/routes/api/summary.go @@ -0,0 +1,37 @@ +package api + +import ( + "github.com/gorilla/mux" + conf "github.com/muety/wakapi/config" + su "github.com/muety/wakapi/routes/utils" + "github.com/muety/wakapi/services" + "github.com/muety/wakapi/utils" + "net/http" +) + +type SummaryApiHandler struct { + config *conf.Config + summarySrvc services.ISummaryService +} + +func NewSummaryApiHandler(summaryService services.ISummaryService) *SummaryApiHandler { + return &SummaryApiHandler{ + summarySrvc: summaryService, + config: conf.Get(), + } +} + +func (h *SummaryApiHandler) RegisterRoutes(router *mux.Router) { + router.Methods(http.MethodGet).HandlerFunc(h.Get) +} + +func (h *SummaryApiHandler) Get(w http.ResponseWriter, r *http.Request) { + summary, err, status := su.LoadUserSummary(h.summarySrvc, r) + if err != nil { + w.WriteHeader(status) + w.Write([]byte(err.Error())) + return + } + + utils.RespondJSON(w, http.StatusOK, summary) +} diff --git a/routes/compat/shields/v1/badge.go b/routes/compat/shields/v1/badge.go index 147637f..9c615da 100644 --- a/routes/compat/shields/v1/badge.go +++ b/routes/compat/shields/v1/badge.go @@ -31,13 +31,12 @@ func NewBadgeHandler(summaryService services.ISummaryService, userService servic } } -func (h *BadgeHandler) RegisterRoutes(router *mux.Router) {} - -func (h *BadgeHandler) RegisterAPIRoutes(router *mux.Router) { - router.Methods(http.MethodGet).HandlerFunc(h.ApiGet) +func (h *BadgeHandler) RegisterRoutes(router *mux.Router) { + r := router.PathPrefix("/shields/v1/{user}").Subrouter() + r.Methods(http.MethodGet).HandlerFunc(h.Get) } -func (h *BadgeHandler) ApiGet(w http.ResponseWriter, r *http.Request) { +func (h *BadgeHandler) Get(w http.ResponseWriter, r *http.Request) { intervalReg := regexp.MustCompile(intervalPattern) entityFilterReg := regexp.MustCompile(entityFilterPattern) diff --git a/routes/compat/wakatime/v1/all_time.go b/routes/compat/wakatime/v1/all_time.go index c7c5532..d78e68e 100644 --- a/routes/compat/wakatime/v1/all_time.go +++ b/routes/compat/wakatime/v1/all_time.go @@ -24,13 +24,11 @@ func NewAllTimeHandler(summaryService services.ISummaryService) *AllTimeHandler } } -func (h *AllTimeHandler) RegisterRoutes(router *mux.Router) {} - -func (h *AllTimeHandler) RegisterAPIRoutes(router *mux.Router) { - router.Path("/all_time_since_today").Methods(http.MethodGet).HandlerFunc(h.ApiGet) +func (h *AllTimeHandler) RegisterRoutes(router *mux.Router) { + router.Path("/wakatime/v1/users/{user}/all_time_since_today").Methods(http.MethodGet).HandlerFunc(h.Get) } -func (h *AllTimeHandler) ApiGet(w http.ResponseWriter, r *http.Request) { +func (h *AllTimeHandler) Get(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) values, _ := url.ParseQuery(r.URL.RawQuery) diff --git a/routes/compat/wakatime/v1/summaries.go b/routes/compat/wakatime/v1/summaries.go index 48ccd6d..a27e9fe 100644 --- a/routes/compat/wakatime/v1/summaries.go +++ b/routes/compat/wakatime/v1/summaries.go @@ -25,10 +25,8 @@ func NewSummariesHandler(summaryService services.ISummaryService) *SummariesHand } } -func (h *SummariesHandler) RegisterRoutes(router *mux.Router) {} - -func (h *SummariesHandler) RegisterAPIRoutes(router *mux.Router) { - router.Path("/summaries").Methods(http.MethodGet).HandlerFunc(h.ApiGet) +func (h *SummariesHandler) RegisterRoutes(router *mux.Router) { + router.Path("/wakatime/v1/users/{user}/summaries").Methods(http.MethodGet).HandlerFunc(h.Get) } // TODO: Support parameters: project, branches, timeout, writes_only, timezone @@ -36,7 +34,7 @@ func (h *SummariesHandler) RegisterAPIRoutes(router *mux.Router) { // Timezone can be specified via an offset suffix (e.g. +02:00) in date strings. // Requires https://github.com/muety/wakapi/issues/108. -func (h *SummariesHandler) ApiGet(w http.ResponseWriter, r *http.Request) { +func (h *SummariesHandler) Get(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) requestedUser := vars["user"] authorizedUser := r.Context().Value(models.UserKey).(*models.User) diff --git a/routes/handler.go b/routes/handler.go index d317880..a9c93b5 100644 --- a/routes/handler.go +++ b/routes/handler.go @@ -4,5 +4,4 @@ import "github.com/gorilla/mux" type Handler interface { RegisterRoutes(router *mux.Router) - RegisterAPIRoutes(router *mux.Router) } diff --git a/routes/health.go b/routes/health.go deleted file mode 100644 index ea497a6..0000000 --- a/routes/health.go +++ /dev/null @@ -1,34 +0,0 @@ -package routes - -import ( - "fmt" - "github.com/gorilla/mux" - "gorm.io/gorm" - "net/http" -) - -type HealthHandler struct { - db *gorm.DB -} - -func NewHealthHandler(db *gorm.DB) *HealthHandler { - return &HealthHandler{db: db} -} - -func (h *HealthHandler) RegisterRoutes(router *mux.Router) {} - -func (h *HealthHandler) RegisterAPIRoutes(router *mux.Router) { - router.Methods(http.MethodGet).HandlerFunc(h.ApiGet) -} - -func (h *HealthHandler) ApiGet(w http.ResponseWriter, r *http.Request) { - var dbStatus int - if sqlDb, err := h.db.DB(); err == nil { - if err := sqlDb.Ping(); err == nil { - dbStatus = 1 - } - } - - w.Header().Set("Content-Type", "text/plain") - w.Write([]byte(fmt.Sprintf("app=1\ndb=%d", dbStatus))) -} diff --git a/routes/home.go b/routes/home.go index 94f44d0..ab82bf1 100644 --- a/routes/home.go +++ b/routes/home.go @@ -32,8 +32,6 @@ func (h *HomeHandler) RegisterRoutes(router *mux.Router) { router.Path("/").Methods(http.MethodGet).HandlerFunc(h.GetIndex) } -func (h *HomeHandler) RegisterAPIRoutes(router *mux.Router) {} - func (h *HomeHandler) GetIndex(w http.ResponseWriter, r *http.Request) { if h.config.IsDev() { loadTemplates() diff --git a/routes/imprint.go b/routes/imprint.go index 546fd50..3d7a9ee 100644 --- a/routes/imprint.go +++ b/routes/imprint.go @@ -25,8 +25,6 @@ func (h *ImprintHandler) RegisterRoutes(router *mux.Router) { router.Path("/imprint").Methods(http.MethodGet).HandlerFunc(h.GetImprint) } -func (h *ImprintHandler) RegisterAPIRoutes(router *mux.Router) {} - func (h *ImprintHandler) GetImprint(w http.ResponseWriter, r *http.Request) { if h.config.IsDev() { loadTemplates() diff --git a/routes/login.go b/routes/login.go index 8b786e4..af3113d 100644 --- a/routes/login.go +++ b/routes/login.go @@ -32,8 +32,6 @@ func (h *LoginHandler) RegisterRoutes(router *mux.Router) { router.Path("/signup").Methods(http.MethodPost).HandlerFunc(h.PostSignup) } -func (h *LoginHandler) RegisterAPIRoutes(router *mux.Router) {} - func (h *LoginHandler) GetIndex(w http.ResponseWriter, r *http.Request) { if h.config.IsDev() { loadTemplates() diff --git a/routes/settings.go b/routes/settings.go index 8da3ad7..f46f1d0 100644 --- a/routes/settings.go +++ b/routes/settings.go @@ -7,6 +7,7 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/schema" conf "github.com/muety/wakapi/config" + "github.com/muety/wakapi/middlewares" "github.com/muety/wakapi/models" "github.com/muety/wakapi/models/view" "github.com/muety/wakapi/services" @@ -41,12 +42,14 @@ func NewSettingsHandler(userService services.IUserService, summaryService servic } func (h *SettingsHandler) RegisterRoutes(router *mux.Router) { - router.Methods(http.MethodGet).HandlerFunc(h.GetIndex) - router.Methods(http.MethodPost).HandlerFunc(h.PostIndex) + r := router.PathPrefix("/settings").Subrouter() + r.Use( + middlewares.NewAuthenticateMiddleware(h.userSrvc, []string{}).Handler, + ) + r.Methods(http.MethodGet).HandlerFunc(h.GetIndex) + r.Methods(http.MethodPost).HandlerFunc(h.PostIndex) } -func (h *SettingsHandler) RegisterAPIRoutes(router *mux.Router) {} - func (h *SettingsHandler) GetIndex(w http.ResponseWriter, r *http.Request) { if h.config.IsDev() { loadTemplates() diff --git a/routes/summary.go b/routes/summary.go index cdca151..42ec1f3 100644 --- a/routes/summary.go +++ b/routes/summary.go @@ -3,8 +3,10 @@ package routes import ( "github.com/gorilla/mux" conf "github.com/muety/wakapi/config" + "github.com/muety/wakapi/middlewares" "github.com/muety/wakapi/models" "github.com/muety/wakapi/models/view" + su "github.com/muety/wakapi/routes/utils" "github.com/muety/wakapi/services" "github.com/muety/wakapi/utils" "net/http" @@ -12,33 +14,24 @@ import ( type SummaryHandler struct { config *conf.Config + userSrvc services.IUserService summarySrvc services.ISummaryService } -func NewSummaryHandler(summaryService services.ISummaryService) *SummaryHandler { +func NewSummaryHandler(summaryService services.ISummaryService, userService services.IUserService) *SummaryHandler { return &SummaryHandler{ summarySrvc: summaryService, + userSrvc: userService, config: conf.Get(), } } func (h *SummaryHandler) RegisterRoutes(router *mux.Router) { - router.Methods(http.MethodGet).HandlerFunc(h.GetIndex) -} - -func (h *SummaryHandler) RegisterAPIRoutes(router *mux.Router) { - router.Methods(http.MethodGet).HandlerFunc(h.ApiGet) -} - -func (h *SummaryHandler) ApiGet(w http.ResponseWriter, r *http.Request) { - summary, err, status := h.loadUserSummary(r) - if err != nil { - w.WriteHeader(status) - w.Write([]byte(err.Error())) - return - } - - utils.RespondJSON(w, http.StatusOK, summary) + r := router.PathPrefix("/summary").Subrouter() + r.Use( + middlewares.NewAuthenticateMiddleware(h.userSrvc, []string{}).Handler, + ) + r.Methods(http.MethodGet).HandlerFunc(h.GetIndex) } func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) { @@ -52,7 +45,7 @@ func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) { r.URL.RawQuery = q.Encode() } - summary, err, status := h.loadUserSummary(r) + summary, err, status := su.LoadUserSummary(h.summarySrvc, r) if err != nil { w.WriteHeader(status) templates[conf.SummaryTemplate].Execute(w, h.buildViewModel(r).WithError(err.Error())) @@ -77,25 +70,6 @@ func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) { templates[conf.SummaryTemplate].Execute(w, vm) } -func (h *SummaryHandler) loadUserSummary(r *http.Request) (*models.Summary, error, int) { - summaryParams, err := utils.ParseSummaryParams(r) - if err != nil { - return nil, err, http.StatusBadRequest - } - - var retrieveSummary services.SummaryRetriever = h.summarySrvc.Retrieve - if summaryParams.Recompute { - retrieveSummary = h.summarySrvc.Summarize - } - - summary, err := h.summarySrvc.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary) - if err != nil { - return nil, err, http.StatusInternalServerError - } - - return summary, nil, http.StatusOK -} - func (h *SummaryHandler) buildViewModel(r *http.Request) *view.SummaryViewModel { return &view.SummaryViewModel{ Success: r.URL.Query().Get("success"), diff --git a/routes/utils/summary_utils.go b/routes/utils/summary_utils.go new file mode 100644 index 0000000..da8a4e1 --- /dev/null +++ b/routes/utils/summary_utils.go @@ -0,0 +1,27 @@ +package utils + +import ( + "github.com/muety/wakapi/models" + "github.com/muety/wakapi/services" + "github.com/muety/wakapi/utils" + "net/http" +) + +func LoadUserSummary(ss services.ISummaryService, r *http.Request) (*models.Summary, error, int) { + summaryParams, err := utils.ParseSummaryParams(r) + if err != nil { + return nil, err, http.StatusBadRequest + } + + var retrieveSummary services.SummaryRetriever = ss.Retrieve + if summaryParams.Recompute { + retrieveSummary = ss.Summarize + } + + summary, err := ss.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary) + if err != nil { + return nil, err, http.StatusInternalServerError + } + + return summary, nil, http.StatusOK +} diff --git a/version.txt b/version.txt index 80a81c0..24b38a9 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.22.0 \ No newline at end of file +1.22.1 \ No newline at end of file