diff --git a/go.mod b/go.mod index afece83..e4a7999 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,9 @@ require ( github.com/codegangsta/negroni v1.0.0 github.com/gobuffalo/packr/v2 v2.8.0 github.com/gorilla/mux v1.7.3 + github.com/gorilla/schema v1.1.0 + github.com/gorilla/securecookie v1.1.1 + github.com/gorilla/sessions v1.2.0 github.com/jasonlvhit/gocron v0.0.0-20191106203602-f82992d443f4 github.com/jinzhu/gorm v1.9.11 github.com/joho/godotenv v1.3.0 diff --git a/go.sum b/go.sum index c46a30c..1c20d02 100644 --- a/go.sum +++ b/go.sum @@ -129,6 +129,12 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY= +github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= +github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= diff --git a/main.go b/main.go index 3dc6ba0..6cd5a66 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "crypto/md5" "encoding/hex" "encoding/json" + "github.com/gorilla/securecookie" "io/ioutil" "log" "net/http" @@ -97,6 +98,12 @@ func readConfig() *models.Config { colors[strings.ToLower(k)] = v.Color } + // TODO: Read keys from env, so that users are not logged out every time the server is restarted + secureCookie := securecookie.New( + securecookie.GenerateRandomKey(64), + securecookie.GenerateRandomKey(32), + ) + return &models.Config{ Env: env, Port: port, @@ -109,6 +116,7 @@ func readConfig() *models.Config { DbDialect: dbType, DbMaxConn: dbMaxConn, CleanUp: cleanUp, + SecureCookie: secureCookie, DefaultUserName: defaultUserName, DefaultUserPassword: defaultUserPassword, CustomLanguages: customLangs, @@ -154,8 +162,8 @@ func main() { summarySrvc := &services.SummaryService{Config: config, Db: db, HeartbeatService: heartbeatSrvc, AliasService: aliasSrvc} aggregationSrvc := &services.AggregationService{Config: config, Db: db, UserService: userSrvc, SummaryService: summarySrvc, HeartbeatService: heartbeatSrvc} - services := []services.Initializable{aliasSrvc, heartbeatSrvc, summarySrvc, userSrvc, aggregationSrvc} - for _, s := range services { + svcs := []services.Initializable{aliasSrvc, heartbeatSrvc, summarySrvc, userSrvc, aggregationSrvc} + for _, s := range svcs { s.Init() } @@ -167,16 +175,13 @@ func main() { } // Handlers - heartbeatHandler := &routes.HeartbeatHandler{HeartbeatSrvc: heartbeatSrvc} - summaryHandler := &routes.SummaryHandler{SummarySrvc: summarySrvc} - healthHandler := &routes.HealthHandler{Db: db} + heartbeatHandler := routes.NewHeartbeatHandler(config, heartbeatSrvc) + summaryHandler := routes.NewSummaryHandler(config, summarySrvc) + healthHandler := routes.NewHealthHandler(db) + indexHandler := routes.NewIndexHandler(config, userSrvc) // Middlewares - authenticateMiddleware := &middlewares.AuthenticateMiddleware{ - UserSrvc: userSrvc, - WhitelistPaths: []string{"/api/health"}, - } - basicAuthMiddleware := &middlewares.RequireBasicAuthMiddleware{} + authenticateMiddleware := middlewares.NewAuthenticateMiddleware(config, userSrvc, []string{"/api/health"}) corsMiddleware := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, AllowedHeaders: []string{"*"}, @@ -186,10 +191,16 @@ func main() { // Setup Routing router := mux.NewRouter() mainRouter := mux.NewRouter().PathPrefix("/").Subrouter() + summaryRouter := mux.NewRouter().PathPrefix("/summary").Subrouter() apiRouter := mux.NewRouter().PathPrefix("/api").Subrouter() // Main Routes - mainRouter.Path("/").Methods(http.MethodGet).HandlerFunc(summaryHandler.Index) + mainRouter.Path("/").Methods(http.MethodGet).HandlerFunc(indexHandler.Index) + mainRouter.Path("/login").Methods(http.MethodPost).HandlerFunc(indexHandler.Login) + mainRouter.Path("/logout").Methods(http.MethodPost).HandlerFunc(indexHandler.Logout) + + // Summary Routes + summaryRouter.Path("").Methods(http.MethodGet).HandlerFunc(summaryHandler.Index) // API Routes apiRouter.Path("/heartbeat").Methods(http.MethodPost).HandlerFunc(heartbeatHandler.ApiPost) @@ -207,9 +218,12 @@ func main() { negroni.Wrap(apiRouter), )) - router.PathPrefix("/").Handler(negroni.Classic().With( - negroni.HandlerFunc(basicAuthMiddleware.Handle), + router.PathPrefix("/summary").Handler(negroni.Classic().With( negroni.HandlerFunc(authenticateMiddleware.Handle), + negroni.Wrap(summaryRouter), + )) + + router.PathPrefix("/").Handler(negroni.Classic().With( negroni.Wrap(mainRouter), )) diff --git a/middlewares/authenticate.go b/middlewares/authenticate.go index 3b0dc57..00607eb 100644 --- a/middlewares/authenticate.go +++ b/middlewares/authenticate.go @@ -2,12 +2,9 @@ package middlewares import ( "context" - "crypto/md5" - "encoding/base64" - "encoding/hex" "errors" + "github.com/muety/wakapi/utils" "net/http" - "regexp" "strings" "time" @@ -18,25 +15,23 @@ import ( ) type AuthenticateMiddleware struct { - UserSrvc *services.UserService - Cache *cache.Cache - WhitelistPaths []string - Initialized bool + config *models.Config + userSrvc *services.UserService + cache *cache.Cache + whitelistPaths []string } -func (m *AuthenticateMiddleware) Init() { - if m.Cache == nil { - m.Cache = cache.New(1*time.Hour, 2*time.Hour) +func NewAuthenticateMiddleware(config *models.Config, userService *services.UserService, whitelistPaths []string) *AuthenticateMiddleware { + return &AuthenticateMiddleware{ + config: config, + userSrvc: userService, + cache: cache.New(1*time.Hour, 2*time.Hour), + whitelistPaths: whitelistPaths, } - m.Initialized = true } func (m *AuthenticateMiddleware) Handle(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - if !m.Initialized { - m.Init() - } - - for _, p := range m.WhitelistPaths { + for _, p := range m.whitelistPaths { if strings.HasPrefix(r.URL.Path, p) || r.URL.Path == p { next(w, r) return @@ -44,81 +39,66 @@ func (m *AuthenticateMiddleware) Handle(w http.ResponseWriter, r *http.Request, } var user *models.User - var userKey string - user, userKey, err := m.tryGetUserByPassword(r) + user, err := m.tryGetUserByCookie(r) if err != nil { - user, userKey, err = m.tryGetUserByApiKey(r) + user, err = m.tryGetUserByApiKey(r) } if err != nil { - w.WriteHeader(http.StatusUnauthorized) + if strings.HasPrefix(r.URL.Path, "/api") { + w.WriteHeader(http.StatusUnauthorized) + } else { + utils.ClearCookie(w, models.AuthCookieKey) + http.Redirect(w, r, "/?error=unauthorized", http.StatusFound) + } return } - m.Cache.Set(userKey, user, cache.DefaultExpiration) + m.cache.Set(user.ID, user, cache.DefaultExpiration) ctx := context.WithValue(r.Context(), models.UserKey, user) next(w, r.WithContext(ctx)) } -func (m *AuthenticateMiddleware) tryGetUserByApiKey(r *http.Request) (*models.User, string, error) { - authHeader := strings.Split(r.Header.Get("Authorization"), " ") - if len(authHeader) != 2 || authHeader[0] != "Basic" { - return nil, "", errors.New("failed to extract API key") - } - - key, err := base64.StdEncoding.DecodeString(authHeader[1]) +func (m *AuthenticateMiddleware) tryGetUserByApiKey(r *http.Request) (*models.User, error) { + key, err := utils.ExtractBearerAuth(r) if err != nil { - return nil, "", err + return nil, err } var user *models.User - userKey := strings.TrimSpace(string(key)) - cachedUser, ok := m.Cache.Get(userKey) + userKey := strings.TrimSpace(key) + cachedUser, ok := m.cache.Get(userKey) if !ok { - user, err = m.UserSrvc.GetUserByKey(userKey) + user, err = m.userSrvc.GetUserByKey(userKey) if err != nil { - return nil, "", err + return nil, err } } else { user = cachedUser.(*models.User) } - return user, userKey, nil + return user, nil } -func (m *AuthenticateMiddleware) tryGetUserByPassword(r *http.Request) (*models.User, string, error) { - authHeader := strings.Split(r.Header.Get("Authorization"), " ") - if len(authHeader) != 2 || authHeader[0] != "Basic" { - return nil, "", errors.New("failed to extract API key") - } - - hash, err := base64.StdEncoding.DecodeString(authHeader[1]) - userKey := strings.TrimSpace(string(hash)) +func (m *AuthenticateMiddleware) tryGetUserByCookie(r *http.Request) (*models.User, error) { + login, err := utils.ExtractCookieAuth(r, m.config) if err != nil { - return nil, "", err + return nil, err } var user *models.User - cachedUser, ok := m.Cache.Get(userKey) + cachedUser, ok := m.cache.Get(login.Username) if !ok { - re := regexp.MustCompile(`^(.+):(.+)$`) - groups := re.FindAllStringSubmatch(userKey, -1) - if len(groups) == 0 || len(groups[0]) != 3 { - return nil, "", errors.New("failed to parse user agent string") - } - userId, password := groups[0][1], groups[0][2] - user, err = m.UserSrvc.GetUserById(userId) + user, err = m.userSrvc.GetUserById(login.Username) if err != nil { - return nil, "", err + return nil, err } - passwordHash := md5.Sum([]byte(password)) - passwordHashString := hex.EncodeToString(passwordHash[:]) - if passwordHashString != user.Password { - return nil, "", errors.New("invalid password") + if !utils.CheckPassword(user, login.Password) { + return nil, errors.New("invalid password") } } else { user = cachedUser.(*models.User) } - return user, userKey, nil + return user, nil } diff --git a/middlewares/basicauth.go b/middlewares/basicauth.go deleted file mode 100644 index 6ab5b72..0000000 --- a/middlewares/basicauth.go +++ /dev/null @@ -1,14 +0,0 @@ -package middlewares - -import ( - "net/http" -) - -type RequireBasicAuthMiddleware struct{} - -func (m *RequireBasicAuthMiddleware) Init() {} - -func (m *RequireBasicAuthMiddleware) Handle(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) - next(w, r) -} diff --git a/models/config.go b/models/config.go index 0e07e15..26c58e7 100644 --- a/models/config.go +++ b/models/config.go @@ -1,21 +1,26 @@ package models +import "github.com/gorilla/securecookie" + type Config struct { - Env string - Port int - Addr string - DbHost string - DbPort uint - DbUser string - DbPassword string - DbName string - DbDialect string - DbMaxConn uint - CleanUp bool - DefaultUserName string - DefaultUserPassword string - CustomLanguages map[string]string - LanguageColors map[string]string + Env string + Port int + Addr string + DbHost string + DbPort uint + DbUser string + DbPassword string + DbName string + DbDialect string + DbMaxConn uint + CleanUp bool + DefaultUserName string + DefaultUserPassword string + SecureCookieHashKey string + SecureCookieBlockKey string + CustomLanguages map[string]string + LanguageColors map[string]string + SecureCookie *securecookie.SecureCookie } func (c *Config) IsDev() bool { diff --git a/models/shared.go b/models/shared.go index 42545f4..109cae6 100644 --- a/models/shared.go +++ b/models/shared.go @@ -1,5 +1,6 @@ package models const ( - UserKey = "user" + UserKey = "user" + AuthCookieKey = "wakapi_auth" ) diff --git a/models/summary.go b/models/summary.go index 3c50196..f662128 100644 --- a/models/summary.go +++ b/models/summary.go @@ -39,4 +39,5 @@ type SummaryItemContainer struct { type SummaryViewModel struct { *Summary LanguageColors map[string]string + Error string } diff --git a/models/user.go b/models/user.go index 7cb5cdc..687949f 100644 --- a/models/user.go +++ b/models/user.go @@ -5,3 +5,8 @@ type User struct { ApiKey string `json:"api_key" gorm:"unique"` Password string `json:"-"` } + +type Login struct { + Username string `json:"username"` + Password string `json:"password"` +} diff --git a/routes/common.go b/routes/common.go new file mode 100644 index 0000000..c902a49 --- /dev/null +++ b/routes/common.go @@ -0,0 +1,43 @@ +package routes + +import ( + "fmt" + "github.com/muety/wakapi/utils" + "html/template" + "io/ioutil" + "path" +) + +func init() { + loadTemplates() +} + +var templates map[string]*template.Template + +func loadTemplates() { + tplPath := "views" + tpls := template.New("").Funcs(template.FuncMap{ + "json": utils.Json, + "date": utils.FormatDateHuman, + }) + templates = make(map[string]*template.Template) + + files, err := ioutil.ReadDir(tplPath) + if err != nil { + panic(err) + } + + for _, file := range files { + tplName := file.Name() + if file.IsDir() || path.Ext(tplName) != ".html" { + continue + } + + tpl, err := tpls.New(tplName).ParseFiles(fmt.Sprintf("%s/%s", tplPath, tplName)) + if err != nil { + panic(err) + } + + templates[tplName] = tpl + } +} diff --git a/routes/health.go b/routes/health.go index c822e01..5f26c60 100644 --- a/routes/health.go +++ b/routes/health.go @@ -7,12 +7,16 @@ import ( ) type HealthHandler struct { - Db *gorm.DB + db *gorm.DB +} + +func NewHealthHandler(db *gorm.DB) *HealthHandler { + return &HealthHandler{db: db} } func (h *HealthHandler) ApiGet(w http.ResponseWriter, r *http.Request) { var dbStatus int - if err := h.Db.DB().Ping(); err == nil { + if err := h.db.DB().Ping(); err == nil { dbStatus = 1 } diff --git a/routes/heartbeat.go b/routes/heartbeat.go index f4c7b0a..c52e82c 100644 --- a/routes/heartbeat.go +++ b/routes/heartbeat.go @@ -12,15 +12,18 @@ import ( ) type HeartbeatHandler struct { - HeartbeatSrvc *services.HeartbeatService + config *models.Config + heartbeatSrvc *services.HeartbeatService +} + +func NewHeartbeatHandler(config *models.Config, heartbearService *services.HeartbeatService) *HeartbeatHandler { + return &HeartbeatHandler{ + config: config, + heartbeatSrvc: heartbearService, + } } func (h *HeartbeatHandler) ApiPost(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - var heartbeats []*models.Heartbeat user := r.Context().Value(models.UserKey).(*models.User) opSys, editor, _ := utils.ParseUserAgent(r.Header.Get("User-Agent")) @@ -37,7 +40,7 @@ func (h *HeartbeatHandler) ApiPost(w http.ResponseWriter, r *http.Request) { hb.Editor = editor hb.User = user hb.UserID = user.ID - hb.Augment(h.HeartbeatSrvc.Config.CustomLanguages) + hb.Augment(h.config.CustomLanguages) if !hb.Valid() { w.WriteHeader(http.StatusBadRequest) @@ -46,7 +49,7 @@ func (h *HeartbeatHandler) ApiPost(w http.ResponseWriter, r *http.Request) { } } - if err := h.HeartbeatSrvc.InsertBatch(heartbeats); err != nil { + if err := h.heartbeatSrvc.InsertBatch(heartbeats); err != nil { w.WriteHeader(http.StatusInternalServerError) os.Stderr.WriteString(err.Error()) return diff --git a/routes/index.go b/routes/index.go new file mode 100644 index 0000000..e8c85ae --- /dev/null +++ b/routes/index.go @@ -0,0 +1,104 @@ +package routes + +import ( + "github.com/gorilla/schema" + "github.com/muety/wakapi/models" + "github.com/muety/wakapi/services" + "github.com/muety/wakapi/utils" + "net/http" +) + +type IndexHandler struct { + config *models.Config + userSrvc *services.UserService +} + +var loginDecoder = schema.NewDecoder() + +func NewIndexHandler(config *models.Config, userService *services.UserService) *IndexHandler { + return &IndexHandler{ + config: config, + userSrvc: userService, + } +} + +func (h *IndexHandler) Index(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, "/summary", http.StatusFound) + return + } + + if err := r.URL.Query().Get("error"); err != "" { + if err == "unauthorized" { + respondError(w, err, http.StatusUnauthorized) + } else { + respondError(w, err, http.StatusInternalServerError) + } + return + } + + templates["index.tpl.html"].Execute(w, nil) +} + +func (h *IndexHandler) Login(w http.ResponseWriter, r *http.Request) { + if h.config.IsDev() { + loadTemplates() + } + + var login models.Login + if err := r.ParseForm(); err != nil { + respondError(w, "missing parameters", http.StatusBadRequest) + return + } + if err := loginDecoder.Decode(&login, r.PostForm); err != nil { + respondError(w, "missing parameters", http.StatusBadRequest) + return + } + + user, err := h.userSrvc.GetUserById(login.Username) + if err != nil { + respondError(w, "resource not found", http.StatusNotFound) + return + } + + if !utils.CheckPassword(user, login.Password) { + respondError(w, "invalid credentials", http.StatusUnauthorized) + return + } + + encoded, err := h.config.SecureCookie.Encode(models.AuthCookieKey, login) + if err != nil { + respondError(w, "internal server error", http.StatusInternalServerError) + return + } + + cookie := &http.Cookie{ + Name: models.AuthCookieKey, + Value: encoded, + Path: "/", + Secure: true, + HttpOnly: true, + } + http.SetCookie(w, cookie) + http.Redirect(w, r, "/summary", http.StatusFound) +} + +func (h *IndexHandler) Logout(w http.ResponseWriter, r *http.Request) { + if h.config.IsDev() { + loadTemplates() + } + + utils.ClearCookie(w, models.AuthCookieKey) + http.Redirect(w, r, "/", http.StatusFound) +} + +func respondError(w http.ResponseWriter, error string, status int) { + w.WriteHeader(status) + templates["index.tpl.html"].Execute(w, struct { + Error string + }{Error: error}) +} diff --git a/routes/summary.go b/routes/summary.go index 56db979..21f7f15 100644 --- a/routes/summary.go +++ b/routes/summary.go @@ -2,11 +2,7 @@ package routes import ( "errors" - "fmt" - "html/template" - "io/ioutil" "net/http" - "path" "time" "github.com/muety/wakapi/models" @@ -24,55 +20,19 @@ const ( ) type SummaryHandler struct { - SummarySrvc *services.SummaryService - Initialized bool - templates map[string]*template.Template + cummarySrvc *services.SummaryService + config *models.Config } -func (m *SummaryHandler) Init() { - m.loadTemplates() - m.Initialized = true -} - -func (m *SummaryHandler) loadTemplates() { - tplPath := "views" - templates := template.New("").Funcs(template.FuncMap{ - "json": utils.Json, - "date": utils.FormatDateHuman, - }) - m.templates = make(map[string]*template.Template) - - files, err := ioutil.ReadDir(tplPath) - if err != nil { - panic(err) - } - - for _, file := range files { - tplName := file.Name() - if file.IsDir() || path.Ext(tplName) != ".html" { - continue - } - - tpl, err := templates.New(tplName).ParseFiles(fmt.Sprintf("%s/%s", tplPath, tplName)) - if err != nil { - panic(err) - } - - m.templates[tplName] = tpl +func NewSummaryHandler(config *models.Config, summaryService *services.SummaryService) *SummaryHandler { + return &SummaryHandler{ + cummarySrvc: summaryService, + config: config, } } func (h *SummaryHandler) ApiGet(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - - if !h.Initialized { - h.Init() - } - - summary, err, status := loadUserSummary(r, h.SummarySrvc) + summary, err, status := loadUserSummary(r, h.cummarySrvc) if err != nil { w.WriteHeader(status) w.Write([]byte(err.Error())) @@ -83,17 +43,8 @@ func (h *SummaryHandler) ApiGet(w http.ResponseWriter, r *http.Request) { } func (h *SummaryHandler) Index(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - - if !h.Initialized { - h.Init() - } - - if h.SummarySrvc.Config.IsDev() { - h.loadTemplates() + if h.config.IsDev() { + loadTemplates() } q := r.URL.Query() @@ -102,7 +53,7 @@ func (h *SummaryHandler) Index(w http.ResponseWriter, r *http.Request) { r.URL.RawQuery = q.Encode() } - summary, err, status := loadUserSummary(r, h.SummarySrvc) + summary, err, status := loadUserSummary(r, h.cummarySrvc) if err != nil { w.WriteHeader(status) w.Write([]byte(err.Error())) @@ -111,10 +62,10 @@ func (h *SummaryHandler) Index(w http.ResponseWriter, r *http.Request) { vm := models.SummaryViewModel{ Summary: summary, - LanguageColors: utils.FilterLanguageColors(h.SummarySrvc.Config.LanguageColors, summary), + LanguageColors: utils.FilterLanguageColors(h.config.LanguageColors, summary), } - h.templates["index.tpl.html"].Execute(w, vm) + templates["summary.tpl.html"].Execute(w, vm) } func loadUserSummary(r *http.Request, summaryService *services.SummaryService) (*models.Summary, error, int) { diff --git a/services/summary.go b/services/summary.go index 01733ba..b66a711 100644 --- a/services/summary.go +++ b/services/summary.go @@ -138,7 +138,7 @@ func (srv *SummaryService) GetByUserWithin(user *models.User, from, to time.Time return summaries, nil } -// Will return *models.Summary objects with only user_id and to_time filled +// Will return *models.Index objects with only user_id and to_time filled func (srv *SummaryService) GetLatestByUser() ([]*models.Summary, error) { var summaries []*models.Summary if err := srv.Db. diff --git a/utils/auth.go b/utils/auth.go new file mode 100644 index 0000000..a164802 --- /dev/null +++ b/utils/auth.go @@ -0,0 +1,65 @@ +package utils + +import ( + "crypto/md5" + "encoding/base64" + "encoding/hex" + "errors" + "github.com/muety/wakapi/models" + "net/http" + "regexp" + "strings" +) + +func ExtractBasicAuth(r *http.Request) (username, password string, err error) { + authHeader := strings.Split(r.Header.Get("Authorization"), " ") + if len(authHeader) != 2 || authHeader[0] != "Basic" { + return username, password, errors.New("failed to extract API key") + } + + hash, err := base64.StdEncoding.DecodeString(authHeader[1]) + userKey := strings.TrimSpace(string(hash)) + if err != nil { + return username, password, err + } + + re := regexp.MustCompile(`^(.+):(.+)$`) + groups := re.FindAllStringSubmatch(userKey, -1) + if len(groups) == 0 || len(groups[0]) != 3 { + return username, password, errors.New("failed to parse user agent string") + } + username, password = groups[0][1], groups[0][2] + return username, password, err +} + +func ExtractBearerAuth(r *http.Request) (key string, err error) { + authHeader := strings.Split(r.Header.Get("Authorization"), " ") + if len(authHeader) != 2 || authHeader[0] != "Basic" { + return key, errors.New("failed to extract API key") + } + + keyBytes, err := base64.StdEncoding.DecodeString(authHeader[1]) + return string(keyBytes), err +} + +func ExtractCookieAuth(r *http.Request, config *models.Config) (login *models.Login, err error) { + cookie, err := r.Cookie(models.AuthCookieKey) + if err != nil { + return nil, errors.New("missing authentication") + } + + if err := config.SecureCookie.Decode(models.AuthCookieKey, cookie.Value, &login); err != nil { + return nil, errors.New("invalid parameters") + } + + return login, nil +} + +func CheckPassword(user *models.User, password string) bool { + passwordHash := md5.Sum([]byte(password)) + passwordHashString := hex.EncodeToString(passwordHash[:]) + if passwordHashString == user.Password { + return true + } + return false +} diff --git a/utils/http.go b/utils/http.go index f5fcf04..b60e9ee 100644 --- a/utils/http.go +++ b/utils/http.go @@ -12,3 +12,13 @@ func RespondJSON(w http.ResponseWriter, status int, object interface{}) { w.WriteHeader(http.StatusInternalServerError) } } + +func ClearCookie(w http.ResponseWriter, name string) { + http.SetCookie(w, &http.Cookie{ + Name: name, + Value: "", + Path: "/", + Secure: true, + HttpOnly: true, + }) +} diff --git a/views/index.tpl.html b/views/index.tpl.html index a723b9b..6b86297 100644 --- a/views/index.tpl.html +++ b/views/index.tpl.html @@ -3,54 +3,36 @@ {{ template "head.tpl.html" . }} -
-

Your Coding Statistics 🤓

-
-
- Today (live) - Yesterday - This Week - This Month - This Year - All Time -
-
-
-
-
-

▶️ {{ .FromTime | date }}

-

{{ .ToTime | date }}

-

-
-
-
-
-
-
- -
-
-
-
- -
-
-
-
- -
-
-
-
- -
-
-
-
+
+

Login

+
- {{ template "footer.tpl.html" . }} +{{ template "alerts.tpl.html" . }} - {{ template "foot.tpl.html" . }} +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +{{ template "footer.tpl.html" . }} + +{{ template "foot.tpl.html" . }} \ No newline at end of file diff --git a/views/summary.tpl.html b/views/summary.tpl.html new file mode 100644 index 0000000..928f087 --- /dev/null +++ b/views/summary.tpl.html @@ -0,0 +1,67 @@ + + +{{ template "head.tpl.html" . }} + + + +
+
+ +
+
+ +
+

Your Coding Statistics 🤓

+
+ +
+ Today (live) + Yesterday + This Week + This Month + This Year + All Time +
+ +{{ template "alerts.tpl.html" . }} + +
+
+
+
+

▶️ {{ .FromTime | date }}

+

{{ .ToTime | date }}

+

+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +{{ template "footer.tpl.html" . }} + +{{ template "foot.tpl.html" . }} + + + \ No newline at end of file