- Refactory cookie and API session handle. Insolated y routes and controllers (api-auth.go, app-auth.go).

- Cookie lifetime with sliding expiration
- Fix bug single global expiresAt in JWT, now each token has unique ExpiresAt
This commit is contained in:
Zeni Kim 2026-09-12 21:26:01 -05:00
parent bf8e2e90ec
commit 10ded026df
10 changed files with 358 additions and 304 deletions

View file

@ -24,12 +24,13 @@ TEMPLATE_ENABLE=true
APP_ENABLE=true APP_ENABLE=true
CDNEnable=false CDNEnable=false
COOKIE_SECRET=13d6b4dff8f84a10851021ec8608f814570d562c92fe6b5ec4c9f595bcb3234b COOKIE_SECRET=13d6b4dff8f84a10851021ec8608f814570d562c92fe6b5ec4c9f595bcb3234b
COOKIE_SECURE=false
####################################### #######################################
###### JWT ###### ###### JWT ######
####################################### #######################################
JWT_SECRET=dkfTgonmgaAdlgkw JWT_SECRET=dkfTgonmgaAdlgkw
JWT_LIFESPAN_MINUTES=1440 # expires after 1 day JWT_LIFESPAN_MINUTES=5 # expires after 1 day
####################################### #######################################
###### DATABASE ###### ###### DATABASE ######

View file

@ -11,7 +11,6 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -23,7 +22,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
func Signup(c *core.Context) *core.Response { func APISignup(c *core.Context) *core.Response {
name := c.GetRequestParam("name") name := c.GetRequestParam("name")
fullname := c.GetRequestParam("fullname") fullname := c.GetRequestParam("fullname")
email := c.GetRequestParam("email") email := c.GetRequestParam("email")
@ -125,17 +124,10 @@ func Signup(c *core.Context) *core.Response {
})) }))
} }
func Signin(c *core.Context) *core.Response { func APISignin(c *core.Context) *core.Response {
email := c.GetRequestParam("email") email := c.GetRequestParam("email")
password := c.GetRequestParam("password") password := c.GetRequestParam("password")
// check if template engine is enable
TemplateEnableStr := os.Getenv("TEMPLATE_ENABLE")
if TemplateEnableStr == "" {
TemplateEnableStr = "false"
}
TemplateEnable, _ := strconv.ParseBool(TemplateEnableStr)
data := map[string]interface{}{ data := map[string]interface{}{
"email": email, "email": email,
"password": password, "password": password,
@ -148,61 +140,36 @@ func Signin(c *core.Context) *core.Response {
if v.Failed() { if v.Failed() {
c.GetLogger().Error(v.GetErrorMessagesJson()) c.GetLogger().Error(v.GetErrorMessagesJson())
if TemplateEnable { return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(v.GetErrorMessagesJson())
// TODO set error in session
return c.Response.Redirect("/applogin")
} else {
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(v.GetErrorMessagesJson())
}
} }
var user models.User var user models.User
res := c.GetGorm().Where("email = ?", c.CastToString(email)).First(&user) res := c.GetGorm().Where("email = ?", c.CastToString(email)).First(&user)
if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) {
c.GetLogger().Error(res.Error.Error()) c.GetLogger().Error(res.Error.Error())
if TemplateEnable { return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{
// TODO set error in session "message": "internal server error",
return c.Response.Redirect("/applogin") }))
} else {
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{
"message": "internal server error",
}))
}
} }
if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) {
if TemplateEnable { return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{
// TODO set error in session "message": "invalid email or password",
return c.Response.Redirect("/applogin") }))
} else {
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{
"message": "invalid email or password",
}))
}
} }
ok, err := c.GetHashing().CheckPasswordHash(user.Password, c.CastToString(password)) ok, err := c.GetHashing().CheckPasswordHash(user.Password, c.CastToString(password))
if err != nil { if err != nil {
c.GetLogger().Error(err.Error()) c.GetLogger().Error(err.Error())
if TemplateEnable { return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{
// TODO set error in session "message": err.Error(),
return c.Response.Redirect("/applogin") }))
} else {
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{
"message": err.Error(),
}))
}
} }
if !ok { if !ok {
if TemplateEnable { return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{
// TODO set error in session "message": "invalid email or password",
return c.Response.Redirect("/applogin") }))
} else {
return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{
"message": "invalid email or password",
}))
}
} }
token, err := c.GetJWT().GenerateToken(map[string]interface{}{ token, err := c.GetJWT().GenerateToken(map[string]interface{}{
@ -211,14 +178,9 @@ func Signin(c *core.Context) *core.Response {
if err != nil { if err != nil {
c.GetLogger().Error(err.Error()) c.GetLogger().Error(err.Error())
// TODO set error in session return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{
if TemplateEnable { "message": "internal server error",
return c.Response.Redirect("/applogin") }))
} else {
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{
"message": "internal server error",
}))
}
} }
// cache the token // cache the token
userAgent := c.GetUserAgent() userAgent := c.GetUserAgent()
@ -232,36 +194,20 @@ func Signin(c *core.Context) *core.Response {
if err != nil { if err != nil {
c.GetLogger().Error(err.Error()) c.GetLogger().Error(err.Error())
if TemplateEnable { return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]interface{}{
// TODO set error in session "message": "internal server error",
return c.Response.Redirect("/applogin")
} else {
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]interface{}{
"message": "internal server error",
}))
}
}
if TemplateEnable {
// create cookie
err = core.SetCookie(c.Response.HttpResponseWriter, email.(string), token)
if err != nil {
panic(fmt.Sprintf("Error write encrypted cookie: %v", err))
return c.Response.SetStatusCode(http.StatusInternalServerError)
}
// redirecto to app
return c.Response.Redirect("/appsample")
} else {
return c.Response.Json(c.MapToJson(map[string]string{
"token": token,
})) }))
} }
return c.Response.Json(c.MapToJson(map[string]string{
"token": token,
}))
} }
func ResetPasswordRequest(c *core.Context) *core.Response { func APIRequestPasswordRequest(c *core.Context) *core.Response {
email := c.GetRequestParam("email") email := c.GetRequestParam("email")
loggr := c.GetLogger()
// validation data // validation data
data := map[string]interface{}{ data := map[string]interface{}{
"email": email, "email": email,
@ -309,6 +255,8 @@ func ResetPasswordRequest(c *core.Context) *core.Response {
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{ return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{
"message": "internal server error", "message": "internal server error",
})) }))
} else {
loggr.Debug(code)
} }
return c.Response.Json(c.MapToJson(map[string]string{ return c.Response.Json(c.MapToJson(map[string]string{
@ -316,7 +264,7 @@ func ResetPasswordRequest(c *core.Context) *core.Response {
})) }))
} }
func SetNewPassword(c *core.Context) *core.Response { func APISetNewPassword(c *core.Context) *core.Response {
urlCode := c.CastToString(c.GetPathParam("code")) urlCode := c.CastToString(c.GetPathParam("code"))
linkCodeDataStr, err := c.GetCache().Get(urlCode) linkCodeDataStr, err := c.GetCache().Get(urlCode)
if err != nil { if err != nil {
@ -427,28 +375,10 @@ func SetNewPassword(c *core.Context) *core.Response {
})) }))
} }
func Signout(c *core.Context) *core.Response { func APISignout(c *core.Context) *core.Response {
// check if template engine is enable tokenRaw := c.GetHeader("Authorization")
TemplateEnableStr := os.Getenv("TEMPLATE_ENABLE") token := strings.TrimSpace(strings.Replace(tokenRaw, "Bearer", "", 1))
if TemplateEnableStr == "" {
TemplateEnableStr = "false"
}
TemplateEnable, _ := strconv.ParseBool(TemplateEnableStr)
token := ""
if TemplateEnable {
// get cookie
usercookie, err := c.GetCookie()
if err != nil {
}
token = usercookie.Token
} else {
tokenRaw := c.GetHeader("Authorization")
token = strings.TrimSpace(strings.Replace(tokenRaw, "Bearer", "", 1))
}
if token == "" { if token == "" {
return c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{ return c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{
@ -475,3 +405,10 @@ func Signout(c *core.Context) *core.Response {
"message": "signed out successfully", "message": "signed out successfully",
})) }))
} }
// This route is token protected by hook
func APIPing(c *core.Context) *core.Response {
return c.Response.SetStatusCode(http.StatusOK).Json(c.MapToJson(map[string]interface{}{
"message": "Pong",
}))
}

201
controllers/app-auth.go Normal file
View file

@ -0,0 +1,201 @@
// Copyright (c) 2026 Zeni Kim <zenik@smarteching.com>
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package controllers
import (
"errors"
"fmt"
"net/http"
"os"
"strconv"
"git.smarteching.com/goffee/core"
"git.smarteching.com/goffee/core/template/components"
"git.smarteching.com/goffee/cup/models"
"gorm.io/gorm"
)
func AppSignin(c *core.Context) *core.Response {
email := c.GetRequestParam("email")
password := c.GetRequestParam("password")
// check if template engine is enable
TemplateEnableStr := os.Getenv("TEMPLATE_ENABLE")
if TemplateEnableStr == "" {
TemplateEnableStr = "false"
}
TemplateEnable, _ := strconv.ParseBool(TemplateEnableStr)
// Apps web only works if template is enabled in environment
if !TemplateEnable {
return c.Response.Redirect("/applogin")
}
data := map[string]interface{}{
"email": email,
"password": password,
}
rules := map[string]interface{}{
"email": "required|email",
"password": "required",
}
v := c.GetValidator().Validate(data, rules)
if v.Failed() {
c.GetLogger().Error(v.GetErrorMessagesJson())
return c.Response.Redirect("/applogin")
}
var user models.User
res := c.GetGorm().Where("email = ?", c.CastToString(email)).First(&user)
if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) {
c.GetLogger().Error(res.Error.Error())
return c.Response.Redirect("/applogin")
}
if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) {
return c.Response.Redirect("/applogin")
}
ok, err := c.GetHashing().CheckPasswordHash(user.Password, c.CastToString(password))
if err != nil {
c.GetLogger().Error(err.Error())
return c.Response.Redirect("/applogin")
}
if !ok {
return c.Response.Redirect("/applogin")
}
token, err := c.GetJWT().GenerateToken(map[string]interface{}{
"userID": user.ID,
})
if err != nil {
c.GetLogger().Error(err.Error())
return c.Response.Redirect("/applogin")
}
// cache the token
userAgent := c.GetUserAgent()
hashedCacheKey := core.CreateAuthTokenHashedCacheKey(user.ID, userAgent)
err = c.GetCache().Set(hashedCacheKey, token)
// delete data from old sessions
sessionKey := fmt.Sprintf("sess_%v", userAgent)
hashedSessionKey := core.CreateAuthTokenHashedCacheKey(user.ID, sessionKey)
_ = c.GetCache().Delete(hashedSessionKey)
if err != nil {
c.GetLogger().Error(err.Error())
// TODO set error in session
return c.Response.Redirect("/applogin")
}
// create cookie
err = core.SetCookie(c.Response.HttpResponseWriter, email.(string), token)
if err != nil {
panic(fmt.Sprintf("Error write encrypted cookie: %v", err))
return c.Response.SetStatusCode(http.StatusInternalServerError)
}
// redirect to app
return c.Response.Redirect("/dashboard")
}
func AppSignout(c *core.Context) *core.Response {
// check if template engine is enable
TemplateEnableStr := os.Getenv("TEMPLATE_ENABLE")
if TemplateEnableStr == "" {
TemplateEnableStr = "false"
}
TemplateEnable, _ := strconv.ParseBool(TemplateEnableStr)
// Apps web only works if template is enabled in environment
if !TemplateEnable {
return c.Response.Redirect("/applogin")
}
token := ""
// get cookie
usercookie, err := c.GetCookie()
if err != nil {
return c.Response.Redirect("/applogin")
}
token = usercookie.Token
if token == "" {
return c.Response.Redirect("/applogin")
}
payload, err := c.GetJWT().DecodeToken(token)
if err != nil {
return c.Response.Redirect("/applogin")
}
userAgent := c.GetUserAgent()
hashedCacheKey := core.CreateAuthTokenHashedCacheKey(uint(c.CastToInt(payload["userID"])), userAgent)
err = c.GetCache().Delete(hashedCacheKey)
if err != nil {
return c.Response.Redirect("/applogin")
}
return c.Response.Redirect("/applogin")
}
// Show basic app login
func AppLogin(c *core.Context) *core.Response {
// check if template engine is enable
TemplateEnableStr := os.Getenv("TEMPLATE_ENABLE")
if TemplateEnableStr == "" {
TemplateEnableStr = "false"
}
TemplateEnable, _ := strconv.ParseBool(TemplateEnableStr)
if TemplateEnable {
// initiate authority
session := c.GetSession()
// true if session is active
hassession := session.Init(c)
// only show login if no has session
if !hassession {
type templateData struct{}
tmplData := templateData{}
return c.Response.Template("login.html", tmplData)
} else {
// first, include all compoments
type templateData struct {
PageCard components.PageCard
}
// now fill data of the components
tmplData := templateData{
PageCard: components.PageCard{
CardTitle: "Golang Framework",
CardBody: "Welcome to Goffee",
},
}
return c.Response.Template("welcome.html", tmplData)
}
} else {
return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]interface{}{
"message": "Apps web only works if template is enabled in environment",
}))
}
}
// This page is cookie protected by hook
func WelcomeToDashboard(c *core.Context) *core.Response {
type templateData struct{}
return c.Response.Template("dashboard.html", templateData{})
}

View file

@ -46,28 +46,3 @@ func WelcomeHome(c *core.Context) *core.Response {
} }
} }
func WelcomeToDashboard(c *core.Context) *core.Response {
message := "{\"message\": \"Welcome to Dashboard\"}"
return c.Response.Json(message)
}
// Show basic app login
func AppLogin(c *core.Context) *core.Response {
// first, include all compoments
// first, include all compoments
type templateData struct {
PageCard components.PageCard
}
// now fill data of the components
tmplData := templateData{
PageCard: components.PageCard{
CardTitle: "Card title",
CardBody: "Loerm ipsum at deim",
},
}
return c.Response.Template("login.html", tmplData)
}

35
go.mod
View file

@ -1,21 +1,22 @@
module git.smarteching.com/goffee/cup module git.smarteching.com/goffee/cup
replace ( replace (
git.smarteching.com/goffee/core => ../core
git.smarteching.com/goffee/cup/config => ./config git.smarteching.com/goffee/cup/config => ./config
git.smarteching.com/goffee/cup/handlers => ./handlers git.smarteching.com/goffee/cup/handlers => ./handlers
git.smarteching.com/goffee/cup/middlewares => ./middlewares git.smarteching.com/goffee/cup/middlewares => ./middlewares
git.smarteching.com/goffee/cup/models => ./models git.smarteching.com/goffee/cup/models => ./models
) )
go 1.25.0 go 1.26.0
require ( require (
git.smarteching.com/goffee/core v1.9.8 git.smarteching.com/goffee/core v1.9.9
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/hibiken/asynq v0.26.0 github.com/hibiken/asynq v0.26.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/julienschmidt/httprouter v1.3.0 github.com/julienschmidt/httprouter v1.3.0
gorm.io/gorm v1.31.1 gorm.io/gorm v1.31.2
) )
require ( require (
@ -26,40 +27,40 @@ require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-chi/chi/v5 v5.3.0 // indirect github.com/go-chi/chi/v5 v5.3.2 // indirect
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect
github.com/go-sql-driver/mysql v1.10.0 // indirect github.com/go-sql-driver/mysql v1.10.1 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/harranali/mailing v1.2.0 // indirect github.com/harranali/mailing v1.2.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.10.0 // indirect github.com/jackc/pgx/v5 v5.11.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/mailgun/errors v0.6.0 // indirect github.com/mailgun/errors v0.6.0 // indirect
github.com/mailgun/mailgun-go/v4 v4.23.0 // indirect github.com/mailgun/mailgun-go/v4 v4.23.0 // indirect
github.com/mattn/go-sqlite3 v1.14.47 // indirect github.com/mattn/go-sqlite3 v1.14.52 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/redis/go-redis/v9 v9.21.0 // indirect github.com/redis/go-redis/v9 v9.22.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/sendgrid/sendgrid-go v3.16.1+incompatible // indirect github.com/sendgrid/sendgrid-go v3.16.1+incompatible // indirect
github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cast v1.10.0 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
golang.org/x/crypto v0.53.0 // indirect golang.org/x/crypto v0.57.0 // indirect
golang.org/x/image v0.43.0 // indirect golang.org/x/image v0.46.0 // indirect
golang.org/x/net v0.56.0 // indirect golang.org/x/net v0.59.0 // indirect
golang.org/x/sync v0.21.0 // indirect golang.org/x/sync v0.23.0 // indirect
golang.org/x/sys v0.46.0 // indirect golang.org/x/sys v0.48.0 // indirect
golang.org/x/text v0.38.0 // indirect golang.org/x/text v0.42.0 // indirect
golang.org/x/time v0.15.0 // indirect golang.org/x/time v0.16.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.12 // indirect
gorm.io/driver/mysql v1.6.0 // indirect gorm.io/driver/mysql v1.6.0 // indirect
gorm.io/driver/postgres v1.6.0 // indirect gorm.io/driver/postgres v1.6.2 // indirect
gorm.io/driver/sqlite v1.6.0 // indirect gorm.io/driver/sqlite v1.6.0 // indirect
) )

84
go.sum
View file

@ -1,7 +1,5 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
git.smarteching.com/goffee/core v1.9.6 h1:GY1EXqbmBEWZAVrl3q22Izb6aXhQzFVQBv2hWhK/So8=
git.smarteching.com/goffee/core v1.9.6/go.mod h1:ifiBgTOR4zCMzdGsabNrEO792EHny2o149NGe3TSlms=
git.smarteching.com/zeni/go-chart/v2 v2.1.4 h1:pF06+F6eqJLIG8uMiTVPR5TygPGMjM/FHMzTxmu5V/Q= git.smarteching.com/zeni/go-chart/v2 v2.1.4 h1:pF06+F6eqJLIG8uMiTVPR5TygPGMjM/FHMzTxmu5V/Q=
git.smarteching.com/zeni/go-chart/v2 v2.1.4/go.mod h1:b3ueW9h3pGGXyhkormZAvilHaG4+mQti+bMNPdQBeOQ= git.smarteching.com/zeni/go-chart/v2 v2.1.4/go.mod h1:b3ueW9h3pGGXyhkormZAvilHaG4+mQti+bMNPdQBeOQ=
git.smarteching.com/zeni/go-charts/v2 v2.6.11 h1:9udzlv3uxGXszpplfkL5IaTUrgkNj++KwhbaN1vVEqI= git.smarteching.com/zeni/go-charts/v2 v2.6.11 h1:9udzlv3uxGXszpplfkL5IaTUrgkNj++KwhbaN1vVEqI=
@ -27,14 +25,12 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE= github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE=
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU=
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME=
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
@ -54,10 +50,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
@ -80,17 +74,13 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailgun/errors v0.5.0 h1:pLQo8uhAdORsjN69mGixSr0pGs46z/BW/FQXd8HG1VM=
github.com/mailgun/errors v0.5.0/go.mod h1:+2nrgY77E0vDkG4ErehpcpbSkMLkseJzKbrva89WeSs=
github.com/mailgun/errors v0.6.0 h1:IWmzIGwXCSN/Q60JT/lXvam3xRAgTUJSX88KwKJ7hss= github.com/mailgun/errors v0.6.0 h1:IWmzIGwXCSN/Q60JT/lXvam3xRAgTUJSX88KwKJ7hss=
github.com/mailgun/errors v0.6.0/go.mod h1:+2nrgY77E0vDkG4ErehpcpbSkMLkseJzKbrva89WeSs= github.com/mailgun/errors v0.6.0/go.mod h1:+2nrgY77E0vDkG4ErehpcpbSkMLkseJzKbrva89WeSs=
github.com/mailgun/mailgun-go/v4 v4.23.0 h1:jPEMJzzin2s7lvehcfv/0UkyBu18GvcURPr2+xtZRbk= github.com/mailgun/mailgun-go/v4 v4.23.0 h1:jPEMJzzin2s7lvehcfv/0UkyBu18GvcURPr2+xtZRbk=
github.com/mailgun/mailgun-go/v4 v4.23.0/go.mod h1:imTtizoFtpfZqPqGP8vltVBB6q9yWcv6llBhfFeElZU= github.com/mailgun/mailgun-go/v4 v4.23.0/go.mod h1:imTtizoFtpfZqPqGP8vltVBB6q9yWcv6llBhfFeElZU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8= github.com/mattn/go-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U=
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/mattn/go-sqlite3 v1.14.47 h1:jOBI62gS7nKeZv+as1oGEy0+1qISgXwH/QBlR6KbfIo=
github.com/mattn/go-sqlite3 v1.14.47/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -102,10 +92,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k= github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0=
github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4=
github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
@ -132,48 +120,36 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.2 h1:BvXQ/cNUg63q5TFNg672DmDcowZSFrNLkkA3Xe6GXq4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/postgres v1.6.2/go.mod h1:0c4fQA44XhOklXDkgtuKqysHCycTa5i9e3EIpDGCwXk=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=

View file

@ -3,8 +3,6 @@ package hooks
import ( import (
"errors" "errors"
"net/http" "net/http"
"os"
"strconv"
"strings" "strings"
"git.smarteching.com/goffee/core" "git.smarteching.com/goffee/core"
@ -14,90 +12,45 @@ import (
var CheckSessionCookie core.Hook = func(c *core.Context) { var CheckSessionCookie core.Hook = func(c *core.Context) {
pass := true pass := false
token := ""
usercookie, err := c.GetCookie()
if err != nil {
} // Validate the cookie based session through the core session. This also
token = usercookie.Token // transparently performs sliding expiration renewal when applicable.
if token == "" { session := c.GetSession()
pass = false if session.Init(c) {
} else { // ensure the authenticated user still exists in the database
payload, err := c.GetJWT().DecodeToken(token) var user models.User
if err != nil { res := c.GetGorm().Where("id = ?", session.GetUserID()).First(&user)
pass = false if res.Error == nil || errors.Is(res.Error, gorm.ErrRecordNotFound) {
} else { pass = true
userAgent := c.GetUserAgent()
hashedCacheKey := core.CreateAuthTokenHashedCacheKey(uint(c.CastToInt(payload["userID"])), userAgent)
cachedToken, err := c.GetCache().Get(hashedCacheKey)
if err != nil {
pass = false
} else if cachedToken != token {
pass = false
} else {
var user models.User
res := c.GetGorm().Where("id = ?", payload["userID"]).First(&user)
if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) {
pass = false
}
}
} }
} }
// if have session redirect protected page
if pass { if pass {
c.Response.Redirect("/appsample").ForceSendResponse() c.Next()
} else {
c.Response.Redirect("/applogin").ForceSendResponse()
return return
} }
c.Next()
} }
var AuthCheck core.Hook = func(c *core.Context) { var APIAuthCheck core.Hook = func(c *core.Context) {
// check if template engine is enable // --- API/bearer flow ---
TemplateEnableStr := os.Getenv("TEMPLATE_ENABLE") tokenRaw := c.GetHeader("Authorization")
if TemplateEnableStr == "" { token := strings.TrimSpace(strings.Replace(tokenRaw, "Bearer", "", 1))
TemplateEnableStr = "false" if token == "" {
} c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{
TemplateEnable, _ := strconv.ParseBool(TemplateEnableStr) "message": "unauthorized",
})).ForceSendResponse()
token := "" return
if TemplateEnable {
usercookie, err := c.GetCookie()
if err != nil {
}
token = usercookie.Token
if token == "" {
c.Response.Redirect("/applogin").ForceSendResponse()
return
}
} else {
tokenRaw := c.GetHeader("Authorization")
token = strings.TrimSpace(strings.Replace(tokenRaw, "Bearer", "", 1))
if token == "" {
c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{
"message": "unauthorized",
})).ForceSendResponse()
return
}
} }
payload, err := c.GetJWT().DecodeToken(token) payload, err := c.GetJWT().DecodeToken(token)
if err != nil { if err != nil {
if TemplateEnable { c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{
c.Response.Redirect("/applogin").ForceSendResponse() "message": "unauthorized",
} else { })).ForceSendResponse()
c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{
"message": "unauthorized",
})).ForceSendResponse()
}
return return
} }
userAgent := c.GetUserAgent() userAgent := c.GetUserAgent()
@ -106,24 +59,16 @@ var AuthCheck core.Hook = func(c *core.Context) {
cachedToken, err := c.GetCache().Get(hashedCacheKey) cachedToken, err := c.GetCache().Get(hashedCacheKey)
if err != nil { if err != nil {
// user signed out // user signed out
if TemplateEnable { c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{
c.Response.Redirect("/applogin").ForceSendResponse() "message": "unauthorized",
} else { })).ForceSendResponse()
c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{
"message": "unauthorized",
})).ForceSendResponse()
}
return return
} }
if cachedToken != token { if cachedToken != token {
// using old token replaced with new one after recent signin // using old token replaced with new one after recent signin
if TemplateEnable { c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{
c.Response.Redirect("/applogin").ForceSendResponse() "message": "unauthorized",
} else { })).ForceSendResponse()
c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{
"message": "unauthorized",
})).ForceSendResponse()
}
return return
} }
@ -132,13 +77,9 @@ var AuthCheck core.Hook = func(c *core.Context) {
if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) {
// error with the database // error with the database
c.GetLogger().Error(res.Error.Error()) c.GetLogger().Error(res.Error.Error())
if TemplateEnable { c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]interface{}{
c.Response.Redirect("/applogin").ForceSendResponse() "message": "internal error",
} else { })).ForceSendResponse()
c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]interface{}{
"message": "internal error",
})).ForceSendResponse()
}
return return
} }

View file

@ -22,11 +22,14 @@ func registerRoutes() {
controller.Get("/", controllers.WelcomeHome) controller.Get("/", controllers.WelcomeHome)
// Uncomment the lines below to enable authentication API // Uncomment the lines below to enable authentication API
controller.Post("/signup", controllers.Signup) controller.Post("/api/signin", controllers.APISignin)
controller.Post("/signin", controllers.Signin) controller.Post("/api/signout", controllers.APISignout)
controller.Post("/signout", controllers.Signout) // API actions
controller.Post("/reset-password", controllers.ResetPasswordRequest) controller.Post("/api/signup", controllers.APISignup)
controller.Post("/reset-password/code/:code", controllers.SetNewPassword) controller.Post("/api/request-password", controllers.APIRequestPasswordRequest)
controller.Post("/api/reset-password/code/:code", controllers.APISetNewPassword)
// API protected route
controller.Get("/api/ping", controllers.APIPing, hooks.APIAuthCheck)
// queue sample route // queue sample route
controller.Get("/queuesample", controllers.Queuesample) controller.Get("/queuesample", controllers.Queuesample)
@ -45,8 +48,12 @@ func registerRoutes() {
controller.Post("/admin/users/delete", controllers.AdminUsersDelete) controller.Post("/admin/users/delete", controllers.AdminUsersDelete)
controller.Post("/admin/users/deleteconfirm", controllers.AdminUsersDelConfirm) controller.Post("/admin/users/deleteconfirm", controllers.AdminUsersDelConfirm)
controller.Get("/dashboard", controllers.WelcomeToDashboard, hooks.AuthCheck) controller.Get("/appsignout", controllers.AppSignout, hooks.CheckSessionCookie)
controller.Get("/signout", controllers.Signout) controller.Get("/applogin", controllers.AppLogin)
controller.Get("/applogin", controllers.AppLogin, hooks.CheckSessionCookie) controller.Post("/applogin", controllers.AppLogin)
controller.Post("/applogin", controllers.AppLogin, hooks.CheckSessionCookie) controller.Post("/appsignin", controllers.AppSignin)
// Cookie protected route
controller.Get("/dashboard", controllers.WelcomeToDashboard, hooks.CheckSessionCookie)
controller.Post("/dashboard", controllers.WelcomeToDashboard, hooks.CheckSessionCookie)
} }

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
{{template "page_head" "Dashhboard page"}}
<body>
<div class="container">
<div class="row">
<h1>Welcome to protected dashboard</h1>
<img class="goffeelogo" style="max-width: 300px;" src="/public/img/gopher_read.png" alt="Gopher read" />
<hr>
<h4><a href="/appsignout">Singout</a></h4>
</div>
</div>
{{template "page_footer"}}
</body>
</html>

View file

@ -4,7 +4,7 @@
<body> <body>
<main> <main>
<form method="POST" action="/signin"> <form method="POST" action="/appsignin">
<div> <div>
<label for="email">Email:</label> <label for="email">Email:</label>
<input type="text" id="email" name="email" required> <input type="text" id="email" name="email" required>