refactor: use cookie-based login

feat: add login page
This commit is contained in:
Ferdinand Mütsch 2020-05-24 13:41:19 +02:00
parent d3ab54f6dc
commit 9697bb5fd5
19 changed files with 449 additions and 219 deletions

3
go.mod
View File

@ -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

6
go.sum
View File

@ -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=

40
main.go
View File

@ -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),
))

View File

@ -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
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -1,5 +1,6 @@
package models
const (
UserKey = "user"
UserKey = "user"
AuthCookieKey = "wakapi_auth"
)

View File

@ -39,4 +39,5 @@ type SummaryItemContainer struct {
type SummaryViewModel struct {
*Summary
LanguageColors map[string]string
Error string
}

View File

@ -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"`
}

43
routes/common.go Normal file
View File

@ -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
}
}

View File

@ -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
}

View File

@ -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

104
routes/index.go Normal file
View File

@ -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})
}

View File

@ -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) {

View File

@ -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.

65
utils/auth.go Normal file
View File

@ -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
}

View File

@ -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,
})
}

View File

@ -3,54 +3,36 @@
{{ template "head.tpl.html" . }}
<body class="bg-gray-800 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
<div class="flex items-center justify-center">
<h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Your Coding Statistics 🤓</h1>
</div>
<div class="text-white text-sm flex items-center justify-center mt-4">
<a href="?interval=today" class="m-1 border-b border-green-700">Today (live)</a>
<a href="?interval=day" class="m-1 border-b border-green-700">Yesterday</a>
<a href="?interval=week" class="m-1 border-b border-green-700">This Week</a>
<a href="?interval=month" class="m-1 border-b border-green-700">This Month</a>
<a href="?interval=year" class="m-1 border-b border-green-700">This Year</a>
<a href="?interval=any" class="m-1 border-b border-green-700">All Time</a>
</div>
<main class="mt-10 flex-grow" id="grid-container">
<div class="flex justify-center">
<div class="p-1">
<div class="flex justify-center p-4 bg-white rounded shadow">
<p class="mx-2"><strong>▶️</strong> <span title="Start Time">{{ .FromTime | date }}</span></p>
<p class="mx-2"><strong></strong> <span title="End Time">{{ .ToTime | date }}</span></p>
<p class="mx-2"><strong></strong> <span id="total-span" title="Total Hours"></span></p>
</div>
</div>
</div>
<div class="flex flex-wrap justify-center">
<div class="w-full lg:w-1/2 p-1">
<div class="p-4 bg-white rounded shadow m-2" id="projects-container" style="height: 300px">
<canvas id="chart-projects"></canvas>
</div>
</div>
<div class="w-full lg:w-1/2 p-1">
<div class="p-4 bg-white rounded shadow m-2" id="os-container" style="height: 300px">
<canvas id="chart-os"></canvas>
</div>
</div>
<div class="w-full lg:w-1/2 p-1">
<div class="p-4 bg-white rounded shadow m-2 relative" id="language-container" style="height: 300px">
<canvas id="chart-language"></canvas>
</div>
</div>
<div class="w-full lg:w-1/2 p-1">
<div class="p-4 bg-white rounded shadow m-2" id="editor-container" style="height: 300px">
<canvas id="chart-editor"></canvas>
</div>
</div>
</div>
</main>
<div class="flex items-center justify-center">
<h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Login</h1>
</div>
{{ template "footer.tpl.html" . }}
{{ template "alerts.tpl.html" . }}
{{ template "foot.tpl.html" . }}
<main class="mt-10 flex-grow flex justify-center w-full" id="grid-container">
<div class="flex-grow max-w-lg mt-12">
<form action="/login" method="post">
<div class="mb-8">
<label class="inline-block text-sm mb-1 text-gray-500" for="username">Username</label>
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3" type="text" id="username"
name="username" placeholder="Enter your username ..." required>
</div>
<div class="mb-8">
<label class="inline-block text-sm mb-1 text-gray-500" for="password">Password</label>
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3" type="password" id="password"
name="password" placeholder="******" required>
</div>
<div class="flex justify-between">
<button type="button" class="py-1 px-3 rounded border border-green-700 text-white text-sm">Sign up</button>
<button type="submit" class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">Log in</button>
</div>
</form>
</div>
</main>
{{ template "footer.tpl.html" . }}
{{ template "foot.tpl.html" . }}
</body>
</html>

67
views/summary.tpl.html Normal file
View File

@ -0,0 +1,67 @@
<html>
{{ template "head.tpl.html" . }}
<body class="relative bg-gray-800 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
<div class="absolute top-0 right-0 mr-8 mt-10 py-2">
<form action="/logout" method="post">
<button type="submit" class="py-1 px-3 rounded border border-green-700 text-white text-sm">Logout</button>
</form>
</div>
<div class="flex items-center justify-center">
<h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Your Coding Statistics 🤓</h1>
</div>
<div class="text-white text-sm flex items-center justify-center mt-4">
<a href="?interval=today" class="m-1 border-b border-green-700">Today (live)</a>
<a href="?interval=day" class="m-1 border-b border-green-700">Yesterday</a>
<a href="?interval=week" class="m-1 border-b border-green-700">This Week</a>
<a href="?interval=month" class="m-1 border-b border-green-700">This Month</a>
<a href="?interval=year" class="m-1 border-b border-green-700">This Year</a>
<a href="?interval=any" class="m-1 border-b border-green-700">All Time</a>
</div>
{{ template "alerts.tpl.html" . }}
<main class="mt-10 flex-grow" id="grid-container">
<div class="flex justify-center">
<div class="p-1">
<div class="flex justify-center p-4 bg-white rounded shadow">
<p class="mx-2"><strong>▶️</strong> <span title="Start Time">{{ .FromTime | date }}</span></p>
<p class="mx-2"><strong></strong> <span title="End Time">{{ .ToTime | date }}</span></p>
<p class="mx-2"><strong></strong> <span id="total-span" title="Total Hours"></span></p>
</div>
</div>
</div>
<div class="flex flex-wrap justify-center">
<div class="w-full lg:w-1/2 p-1">
<div class="p-4 bg-white rounded shadow m-2" id="projects-container" style="height: 300px">
<canvas id="chart-projects"></canvas>
</div>
</div>
<div class="w-full lg:w-1/2 p-1">
<div class="p-4 bg-white rounded shadow m-2" id="os-container" style="height: 300px">
<canvas id="chart-os"></canvas>
</div>
</div>
<div class="w-full lg:w-1/2 p-1">
<div class="p-4 bg-white rounded shadow m-2 relative" id="language-container" style="height: 300px">
<canvas id="chart-language"></canvas>
</div>
</div>
<div class="w-full lg:w-1/2 p-1">
<div class="p-4 bg-white rounded shadow m-2" id="editor-container" style="height: 300px">
<canvas id="chart-editor"></canvas>
</div>
</div>
</div>
</main>
{{ template "footer.tpl.html" . }}
{{ template "foot.tpl.html" . }}
</body>
</html>