From 16b9aa22821c0c82bb6814a9f55844c533780771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferdinand=20M=C3=BCtsch?= Date: Sat, 28 Nov 2020 20:23:40 +0100 Subject: [PATCH] feat: add front page (resolve #34) --- config/templates.go | 1 + main.go | 12 ++- models/view/login.go | 16 ++++ routes/home.go | 126 +------------------------ routes/login.go | 159 ++++++++++++++++++++++++++++++++ static/assets/images/ghicon.svg | 1 + version.txt | 2 +- views/imprint.tpl.html | 3 + views/index.tpl.html | 66 ++++++++----- views/settings.tpl.html | 9 +- views/summary.tpl.html | 2 + 11 files changed, 241 insertions(+), 156 deletions(-) create mode 100644 models/view/login.go create mode 100644 routes/login.go create mode 100644 static/assets/images/ghicon.svg diff --git a/config/templates.go b/config/templates.go index c7c94f8..0463154 100644 --- a/config/templates.go +++ b/config/templates.go @@ -2,6 +2,7 @@ package config const ( IndexTemplate = "index.tpl.html" + LoginTemplate = "login.tpl.html" ImprintTemplate = "imprint.tpl.html" SignupTemplate = "signup.tpl.html" SettingsTemplate = "settings.tpl.html" diff --git a/main.go b/main.go index 2f4a939..5f1b05f 100644 --- a/main.go +++ b/main.go @@ -109,7 +109,8 @@ func main() { healthHandler := routes.NewHealthHandler(db) heartbeatHandler := routes.NewHeartbeatHandler(heartbeatService, languageMappingService) settingsHandler := routes.NewSettingsHandler(userService, summaryService, aggregationService, languageMappingService) - homeHandler := routes.NewHomeHandler(userService) + homeHandler := routes.NewHomeHandler() + loginHandler := routes.NewLoginHandler(userService) imprintHandler := routes.NewImprintHandler(keyValueService) wakatimeV1AllHandler := wtV1Routes.NewAllTimeHandler(summaryService) wakatimeV1SummariesHandler := wtV1Routes.NewSummariesHandler(summaryService) @@ -142,10 +143,11 @@ func main() { // Public Routes publicRouter.Path("/").Methods(http.MethodGet).HandlerFunc(homeHandler.GetIndex) - publicRouter.Path("/login").Methods(http.MethodPost).HandlerFunc(homeHandler.PostLogin) - publicRouter.Path("/logout").Methods(http.MethodPost).HandlerFunc(homeHandler.PostLogout) - publicRouter.Path("/signup").Methods(http.MethodGet).HandlerFunc(homeHandler.GetSignup) - publicRouter.Path("/signup").Methods(http.MethodPost).HandlerFunc(homeHandler.PostSignup) + publicRouter.Path("/login").Methods(http.MethodGet).HandlerFunc(loginHandler.GetIndex) + publicRouter.Path("/login").Methods(http.MethodPost).HandlerFunc(loginHandler.PostLogin) + publicRouter.Path("/logout").Methods(http.MethodPost).HandlerFunc(loginHandler.PostLogout) + publicRouter.Path("/signup").Methods(http.MethodGet).HandlerFunc(loginHandler.GetSignup) + publicRouter.Path("/signup").Methods(http.MethodPost).HandlerFunc(loginHandler.PostSignup) publicRouter.Path("/imprint").Methods(http.MethodGet).HandlerFunc(imprintHandler.GetImprint) // Summary Routes diff --git a/models/view/login.go b/models/view/login.go new file mode 100644 index 0000000..1338790 --- /dev/null +++ b/models/view/login.go @@ -0,0 +1,16 @@ +package view + +type LoginViewModel struct { + Success string + Error string +} + +func (s *LoginViewModel) WithSuccess(m string) *LoginViewModel { + s.Success = m + return s +} + +func (s *LoginViewModel) WithError(m string) *LoginViewModel { + s.Error = m + return s +} diff --git a/routes/home.go b/routes/home.go index 962697e..272156d 100644 --- a/routes/home.go +++ b/routes/home.go @@ -4,26 +4,21 @@ import ( "fmt" "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" "net/http" - "time" ) type HomeHandler struct { - config *conf.Config - userSrvc services.IUserService + config *conf.Config } var loginDecoder = schema.NewDecoder() var signupDecoder = schema.NewDecoder() -func NewHomeHandler(userService services.IUserService) *HomeHandler { +func NewHomeHandler() *HomeHandler { return &HomeHandler{ - config: conf.Get(), - userSrvc: userService, + config: conf.Get(), } } @@ -40,121 +35,6 @@ func (h *HomeHandler) GetIndex(w http.ResponseWriter, r *http.Request) { templates[conf.IndexTemplate].Execute(w, h.buildViewModel(r)) } -func (h *HomeHandler) PostLogin(w http.ResponseWriter, r *http.Request) { - if h.config.IsDev() { - loadTemplates() - } - - if cookie, err := r.Cookie(models.AuthCookieKey); err == nil && cookie.Value != "" { - http.Redirect(w, r, fmt.Sprintf("%s/summary", h.config.Server.BasePath), http.StatusFound) - return - } - - var login models.Login - if err := r.ParseForm(); err != nil { - w.WriteHeader(http.StatusBadRequest) - templates[conf.IndexTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters")) - return - } - if err := loginDecoder.Decode(&login, r.PostForm); err != nil { - w.WriteHeader(http.StatusBadRequest) - templates[conf.IndexTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters")) - return - } - - user, err := h.userSrvc.GetUserById(login.Username) - if err != nil { - w.WriteHeader(http.StatusNotFound) - templates[conf.IndexTemplate].Execute(w, h.buildViewModel(r).WithError("resource not found")) - return - } - - // TODO: depending on middleware package here is a hack - if !middlewares.CheckAndMigratePassword(user, &login, h.config.Security.PasswordSalt, &h.userSrvc) { - w.WriteHeader(http.StatusUnauthorized) - templates[conf.IndexTemplate].Execute(w, h.buildViewModel(r).WithError("invalid credentials")) - return - } - - encoded, err := h.config.Security.SecureCookie.Encode(models.AuthCookieKey, login) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - templates[conf.IndexTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error")) - return - } - - user.LastLoggedInAt = models.CustomTime(time.Now()) - h.userSrvc.Update(user) - - http.SetCookie(w, h.config.CreateCookie(models.AuthCookieKey, encoded, "/")) - http.Redirect(w, r, fmt.Sprintf("%s/summary", h.config.Server.BasePath), http.StatusFound) -} - -func (h *HomeHandler) PostLogout(w http.ResponseWriter, r *http.Request) { - if h.config.IsDev() { - loadTemplates() - } - - http.SetCookie(w, h.config.GetClearCookie(models.AuthCookieKey, "/")) - http.Redirect(w, r, fmt.Sprintf("%s/", h.config.Server.BasePath), http.StatusFound) -} - -func (h *HomeHandler) GetSignup(w http.ResponseWriter, r *http.Request) { - if h.config.IsDev() { - loadTemplates() - } - - if cookie, err := r.Cookie(models.AuthCookieKey); err == nil && cookie.Value != "" { - http.Redirect(w, r, fmt.Sprintf("%s/summary", h.config.Server.BasePath), http.StatusFound) - return - } - - templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r)) -} - -func (h *HomeHandler) PostSignup(w http.ResponseWriter, r *http.Request) { - if h.config.IsDev() { - loadTemplates() - } - - if cookie, err := r.Cookie(models.AuthCookieKey); err == nil && cookie.Value != "" { - http.Redirect(w, r, fmt.Sprintf("%s/summary", h.config.Server.BasePath), http.StatusFound) - return - } - - var signup models.Signup - if err := r.ParseForm(); err != nil { - w.WriteHeader(http.StatusBadRequest) - templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters")) - return - } - if err := signupDecoder.Decode(&signup, r.PostForm); err != nil { - w.WriteHeader(http.StatusBadRequest) - templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters")) - return - } - - if !signup.IsValid() { - w.WriteHeader(http.StatusBadRequest) - templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("invalid parameters")) - return - } - - _, created, err := h.userSrvc.CreateOrGet(&signup) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("failed to create new user")) - return - } - if !created { - w.WriteHeader(http.StatusConflict) - templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("user already existing")) - return - } - - http.Redirect(w, r, fmt.Sprintf("%s/?success=%s", h.config.Server.BasePath, "account created successfully"), http.StatusFound) -} - func (h *HomeHandler) buildViewModel(r *http.Request) *view.HomeViewModel { return &view.HomeViewModel{ Success: r.URL.Query().Get("success"), diff --git a/routes/login.go b/routes/login.go new file mode 100644 index 0000000..5c99bbd --- /dev/null +++ b/routes/login.go @@ -0,0 +1,159 @@ +package routes + +import ( + "fmt" + 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" + "net/http" + "time" +) + +type LoginHandler struct { + config *conf.Config + userSrvc services.IUserService +} + +func NewLoginHandler(userService services.IUserService) *LoginHandler { + return &LoginHandler{ + config: conf.Get(), + userSrvc: userService, + } +} + +func (h *LoginHandler) GetIndex(w http.ResponseWriter, r *http.Request) { + if h.config.IsDev() { + loadTemplates() + } + + if cookie, err := r.Cookie(models.AuthCookieKey); err == nil && cookie.Value != "" { + http.Redirect(w, r, fmt.Sprintf("%s/summary", h.config.Server.BasePath), http.StatusFound) + return + } + + templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r)) +} + +func (h *LoginHandler) PostLogin(w http.ResponseWriter, r *http.Request) { + if h.config.IsDev() { + loadTemplates() + } + + if cookie, err := r.Cookie(models.AuthCookieKey); err == nil && cookie.Value != "" { + http.Redirect(w, r, fmt.Sprintf("%s/summary", h.config.Server.BasePath), http.StatusFound) + return + } + + var login models.Login + if err := r.ParseForm(); err != nil { + w.WriteHeader(http.StatusBadRequest) + templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters")) + return + } + if err := loginDecoder.Decode(&login, r.PostForm); err != nil { + w.WriteHeader(http.StatusBadRequest) + templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters")) + return + } + + user, err := h.userSrvc.GetUserById(login.Username) + if err != nil { + w.WriteHeader(http.StatusNotFound) + templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r).WithError("resource not found")) + return + } + + // TODO: depending on middleware package here is a hack + if !middlewares.CheckAndMigratePassword(user, &login, h.config.Security.PasswordSalt, &h.userSrvc) { + w.WriteHeader(http.StatusUnauthorized) + templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r).WithError("invalid credentials")) + return + } + + encoded, err := h.config.Security.SecureCookie.Encode(models.AuthCookieKey, login) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error")) + return + } + + user.LastLoggedInAt = models.CustomTime(time.Now()) + h.userSrvc.Update(user) + + http.SetCookie(w, h.config.CreateCookie(models.AuthCookieKey, encoded, "/")) + http.Redirect(w, r, fmt.Sprintf("%s/summary", h.config.Server.BasePath), http.StatusFound) +} + +func (h *LoginHandler) PostLogout(w http.ResponseWriter, r *http.Request) { + if h.config.IsDev() { + loadTemplates() + } + + http.SetCookie(w, h.config.GetClearCookie(models.AuthCookieKey, "/")) + http.Redirect(w, r, fmt.Sprintf("%s/", h.config.Server.BasePath), http.StatusFound) +} + +func (h *LoginHandler) GetSignup(w http.ResponseWriter, r *http.Request) { + if h.config.IsDev() { + loadTemplates() + } + + if cookie, err := r.Cookie(models.AuthCookieKey); err == nil && cookie.Value != "" { + http.Redirect(w, r, fmt.Sprintf("%s/summary", h.config.Server.BasePath), http.StatusFound) + return + } + + templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r)) +} + +func (h *LoginHandler) PostSignup(w http.ResponseWriter, r *http.Request) { + if h.config.IsDev() { + loadTemplates() + } + + if cookie, err := r.Cookie(models.AuthCookieKey); err == nil && cookie.Value != "" { + http.Redirect(w, r, fmt.Sprintf("%s/summary", h.config.Server.BasePath), http.StatusFound) + return + } + + var signup models.Signup + if err := r.ParseForm(); err != nil { + w.WriteHeader(http.StatusBadRequest) + templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters")) + return + } + if err := signupDecoder.Decode(&signup, r.PostForm); err != nil { + w.WriteHeader(http.StatusBadRequest) + templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters")) + return + } + + if !signup.IsValid() { + w.WriteHeader(http.StatusBadRequest) + templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("invalid parameters")) + return + } + + _, created, err := h.userSrvc.CreateOrGet(&signup) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("failed to create new user")) + return + } + if !created { + w.WriteHeader(http.StatusConflict) + templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("user already existing")) + return + } + + http.Redirect(w, r, fmt.Sprintf("%s/?success=%s", h.config.Server.BasePath, "account created successfully"), http.StatusFound) +} + +func (h *LoginHandler) buildViewModel(r *http.Request) *view.LoginViewModel { + return &view.LoginViewModel{ + Success: r.URL.Query().Get("success"), + Error: r.URL.Query().Get("error"), + } +} diff --git a/static/assets/images/ghicon.svg b/static/assets/images/ghicon.svg new file mode 100644 index 0000000..5be4ef3 --- /dev/null +++ b/static/assets/images/ghicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/version.txt b/version.txt index a232073..092afa1 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.16.4 +1.17.0 diff --git a/views/imprint.tpl.html b/views/imprint.tpl.html index fa251dc..23da03d 100644 --- a/views/imprint.tpl.html +++ b/views/imprint.tpl.html @@ -4,6 +4,9 @@ {{ template "head.tpl.html" . }} + +{{ template "header.tpl.html" . }} +
← Go back
diff --git a/views/index.tpl.html b/views/index.tpl.html index d1e6d30..6104bc2 100644 --- a/views/index.tpl.html +++ b/views/index.tpl.html @@ -3,35 +3,53 @@ {{ template "head.tpl.html" . }} - -
-

Login

-
+ + +{{ template "header.tpl.html" . }} {{ template "alerts.tpl.html" . }} + +
-
-
-
- - +
+

Keep Track of Your Coding Time πŸ•“

+

Wakapi is an open-source tool that helps you keep track of the time you have spent coding on different projects in different programming languages and more. Ideal for statistics freaks any anyone else.

+ +
+ +
+
+

Features

+
+
    +
  • βœ…   100 % free and open-source
  • +
  • βœ…   Built by developers for developers
  • +
  • βœ…   Fancy statistics and plots
  • +
  • βœ…   Cool badges for readmes
  • +
  • βœ…   Intuitive REST API
  • +
  • βœ…   Compatible with Wakatime
  • +
  • βœ…   Prometheus metrics via exporter
  • +
  • βœ…   Self-hosted
  • +
-
- - -
-
- - - - -
- +
diff --git a/views/settings.tpl.html b/views/settings.tpl.html index 72217ea..2db68b2 100644 --- a/views/settings.tpl.html +++ b/views/settings.tpl.html @@ -4,18 +4,21 @@ {{ template "head.tpl.html" . }} + +{{ template "header.tpl.html" . }} +
-
+

Settings

-
+
         
{{ template "alerts.tpl.html" . }}
-
+
Change Password diff --git a/views/summary.tpl.html b/views/summary.tpl.html index 359079d..19205a3 100644 --- a/views/summary.tpl.html +++ b/views/summary.tpl.html @@ -5,6 +5,8 @@ +{{ template "header.tpl.html" . }} +