diff --git a/.env-dev b/.env-dev index 9157a9c..434377e 100644 --- a/.env-dev +++ b/.env-dev @@ -24,12 +24,13 @@ TEMPLATE_ENABLE=true APP_ENABLE=true CDNEnable=false COOKIE_SECRET=13d6b4dff8f84a10851021ec8608f814570d562c92fe6b5ec4c9f595bcb3234b +COOKIE_SECURE=false ####################################### ###### JWT ###### ####################################### JWT_SECRET=dkfTgonmgaAdlgkw -JWT_LIFESPAN_MINUTES=1440 # expires after 1 day +JWT_LIFESPAN_MINUTES=5 # expires after 1 day ####################################### ###### DATABASE ###### diff --git a/README.md b/README.md index 4ae95df..7a8c574 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ Cup is a skeleton project for the Goffee [Go](https://go.dev) framework made fo - JWT tokens - Cache (Redis) - HTTPS (TLS) +- Queues ([asynq](https://github.com/hibiken/asynq)) +- Scheduler (DB-backed task runner) +- Server-side template rendering ## Installation To create a new `cup` project you need to install the `Goffee's cli` first @@ -67,26 +70,97 @@ The first component that receive's the request in `Cup` is the `Router`, then `Goffee` locates the matching [handler](https://git.smarteching.com/goffee/docs/handlers) of the request and it check's if there are any [hooks](https://git.smarteching.com/goffee/docs/hooks) to be executed either before or after the [controller](https://git.smarteching.com/goffee/controllers), if so, it executes them in the right order, then at the final stage it returns the response to the user. `Request -> Router -> Optional Hooks -> Controller -> Optional Hooks -> Response` +## Configuration +The application is configured through the `config/` package. Each file exposes a single +`Get...Config()` function that returns a `core.*Config` struct. The most important toggles are: + +| File | Function | Controls | +| --- | --- | --- | +| `config/dotenvfile.go` | `GetEnvFileConfig()` | Whether to load variables from the `.env` file | +| `config/request.go` | `GetRequestConfig()` | Max upload file size | +| `config/gorm.go` | `GetGormConfig()` | Enables the GORM database layer | +| `config/cache.go` | `GetCacheConfig()` | Enables the cache (Redis) | +| `config/queue.go` | `GetQueueConfig()` | Enables the queue system (asynq) | +| `config/scheduler.go` | `GetSchedulerConfig()` | Enables the DB-backed scheduler | + +The enabled features are wired up in `main.go`. For example, queues are only started when +`GetQueueConfig().EnableQueue` is `true`, and the scheduler only when +`GetSchedulerConfig().EnableScheduler` is `true`. + +## Background jobs + +Cup ships with two independent background processing systems, both disabled by default. + +### Queues (asynq) +The queue system is built on [asynq](https://github.com/hibiken/asynq). Register your task +handlers in `register-queues.go`: + +```go +queque.AddWork(workers.TypeWelcomeEmail, workers.HandleWelcomeEmailTask) +``` + +Task types and handlers live under `workers/`. To enqueue a task from a controller use the +queue client, as shown in `controllers/queuesample.go`: + +```go +client := c.GetQueueClient() +client.Enqueue(asynq.NewTask(workers.TypeWelcomeEmail, payload)) +``` + +### Scheduler +The scheduler is a lightweight, DB-backed task runner. Register handlers in +`register-scheduler.go`: + +```go +sched.AddWork("send_email", handleSendEmail) +``` + +Tasks are enqueued into the `queue_items` table and can run multiple times, with +priorities and concurrency. See `controllers/schedsample.go` for examples of enqueuing +tasks with different `maxRuns` / `thread` / priority values. + +### Managing the scheduler from the CLI +The `goffee` cli provides commands to inspect and manage the scheduler tables. They must be +run from the project directory: + +```bash +goffee scheduler:queue dev # list pending/processing tasks +goffee scheduler:processed dev 20 # list the last 20 processed executions +goffee scheduler:semaphore dev # show the semaphore state +goffee scheduler:semaphore dev red # block execution (after restart) +goffee scheduler:truncate dev # empty the queue and processed logs +``` + ## Folder structure ```bash ├── cup │ ├── config/ --------------------------> main configs -│ ├── events/ --------------------------> contains events -│ │ ├── jobs/ ------------------------> contains the event jobs │ ├── controllers/ ---------------------> route's controllers -│ ├── logs/ ----------------------------> app log files +│ ├── events/ --------------------------> contains events +│ │ ├── event-names.go ---------------> event name constants +│ │ └── eventjobs/ -------------------> contains the event jobs │ ├── hooks/ ---------------------------> app hooks +│ ├── logs/ ----------------------------> app log files │ ├── models/ --------------------------> database models │ ├── storage/ -------------------------> a place to store files +│ │ ├── app/ -------------------------> static files served under /app +│ │ ├── cdn/ -------------------------> static files served under /cdn +│ │ ├── public/ ----------------------> static files served under /public +│ │ ├── sqlite/ ----------------------> sqlite database file +│ │ └── templates/ -------------------> server-side templates │ ├── tls/ -----------------------------> tls certificates -│ ├── .env -----------------------------> environment variables +│ ├── utils/ ---------------------------> shared helpers and seed data +│ ├── workers/ -------------------------> queue task types and handlers +│ ├── .env -----------------------------> environment variables │ ├── .gitignore -----------------------> .gitignore │ ├── go.mod ---------------------------> Go modules │ ├── LICENSE --------------------------> license │ ├── main.go --------------------------> go main file │ ├── README.md ------------------------> readme file │ ├── register-events.go ---------------> register events and jobs -│ ├── register-global-hooks.go ---------> register global middlewares +│ ├── register-global-hooks.go ---------> register global hooks +│ ├── register-queues.go ---------------> register queue workers +│ ├── register-scheduler.go ------------> register scheduler tasks │ ├── routes.go ------------------------> app routes │ ├── run-auto-migrations.go -----------> database migrations -``` \ No newline at end of file +``` diff --git a/controllers/authentication.go b/controllers/api-auth.go similarity index 78% rename from controllers/authentication.go rename to controllers/api-auth.go index 35ab468..906a0cf 100644 --- a/controllers/authentication.go +++ b/controllers/api-auth.go @@ -11,7 +11,6 @@ import ( "fmt" "net/http" - "os" "strconv" "strings" "time" @@ -23,7 +22,7 @@ import ( "gorm.io/gorm" ) -func Signup(c *core.Context) *core.Response { +func APISignup(c *core.Context) *core.Response { name := c.GetRequestParam("name") fullname := c.GetRequestParam("fullname") 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") 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{}{ "email": email, "password": password, @@ -148,61 +140,36 @@ func Signin(c *core.Context) *core.Response { if v.Failed() { c.GetLogger().Error(v.GetErrorMessagesJson()) - if TemplateEnable { - // TODO set error in session - return c.Response.Redirect("/applogin") - } else { - return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(v.GetErrorMessagesJson()) - } + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(v.GetErrorMessagesJson()) } 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()) - if TemplateEnable { - // TODO set error in session - return c.Response.Redirect("/applogin") - } else { - return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{ - "message": "internal server error", - })) - } + 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 TemplateEnable { - // TODO set error in session - return c.Response.Redirect("/applogin") - } else { - return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{ - "message": "invalid email or password", - })) - } + 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)) if err != nil { c.GetLogger().Error(err.Error()) - if TemplateEnable { - // TODO set error in session - return c.Response.Redirect("/applogin") - } else { - return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{ - "message": err.Error(), - })) - } + return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{ + "message": err.Error(), + })) } if !ok { - if TemplateEnable { - // TODO set error in session - return c.Response.Redirect("/applogin") - } else { - return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{ - "message": "invalid email or password", - })) - } + 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{}{ @@ -211,14 +178,9 @@ func Signin(c *core.Context) *core.Response { if err != nil { c.GetLogger().Error(err.Error()) - // TODO set error in session - if TemplateEnable { - return c.Response.Redirect("/applogin") - } else { - return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{ - "message": "internal server error", - })) - } + return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{ + "message": "internal server error", + })) } // cache the token userAgent := c.GetUserAgent() @@ -232,36 +194,20 @@ func Signin(c *core.Context) *core.Response { if err != nil { c.GetLogger().Error(err.Error()) - if TemplateEnable { - // TODO set error in session - 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.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]interface{}{ + "message": "internal server error", })) } + + 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") - + loggr := c.GetLogger() // validation data data := map[string]interface{}{ "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{ "message": "internal server error", })) + } else { + loggr.Debug(code) } 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")) linkCodeDataStr, err := c.GetCache().Get(urlCode) 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 - TemplateEnableStr := os.Getenv("TEMPLATE_ENABLE") - 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)) - } + tokenRaw := c.GetHeader("Authorization") + token := strings.TrimSpace(strings.Replace(tokenRaw, "Bearer", "", 1)) if token == "" { 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", })) } + +// 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", + })) +} diff --git a/controllers/app-auth.go b/controllers/app-auth.go new file mode 100644 index 0000000..32ed3f3 --- /dev/null +++ b/controllers/app-auth.go @@ -0,0 +1,213 @@ +// Copyright (c) 2026 Zeni Kim +// 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 { + // the cookie could not be read/decrypted, still make sure it is removed from the client + core.ClearCookie(c.Response.HttpResponseWriter) + return c.Response.Redirect("/applogin") + } + token = usercookie.Token + + if token == "" { + // no token present, still clear any (possibly present) cookie on the client + core.ClearCookie(c.Response.HttpResponseWriter) + return c.Response.Redirect("/applogin") + } + payload, err := c.GetJWT().DecodeToken(token) + if err != nil { + // the token is invalid, clear the cookie on the client + core.ClearCookie(c.Response.HttpResponseWriter) + 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 { + // failed to invalidate the cached token, still clear the cookie on the client + core.ClearCookie(c.Response.HttpResponseWriter) + return c.Response.Redirect("/applogin") + } + + // successful signout: the token is removed from the cache, also clear + // the cookie on the client before the final redirect + core.ClearCookie(c.Response.HttpResponseWriter) + + 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{}) + +} diff --git a/controllers/home.go b/controllers/home.go index ccc7ac2..4f67777 100644 --- a/controllers/home.go +++ b/controllers/home.go @@ -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) -} diff --git a/go.mod b/go.mod index 9fa4902..d812061 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,22 @@ module git.smarteching.com/goffee/cup replace ( + git.smarteching.com/goffee/core => ../core git.smarteching.com/goffee/cup/config => ./config git.smarteching.com/goffee/cup/handlers => ./handlers git.smarteching.com/goffee/cup/middlewares => ./middlewares git.smarteching.com/goffee/cup/models => ./models ) -go 1.25.0 +go 1.26.0 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/hibiken/asynq v0.26.0 github.com/joho/godotenv v1.5.1 github.com/julienschmidt/httprouter v1.3.0 - gorm.io/gorm v1.31.1 + gorm.io/gorm v1.31.2 ) require ( @@ -26,40 +27,40 @@ require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // 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-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/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/harranali/mailing v1.2.0 // indirect github.com/jackc/pgpassfile v1.0.0 // 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/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailgun/errors v0.6.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/reflect2 v1.0.2 // 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/sendgrid/rest v2.6.9+incompatible // indirect github.com/sendgrid/sendgrid-go v3.16.1+incompatible // indirect github.com/spf13/cast v1.10.0 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/crypto v0.53.0 // indirect - golang.org/x/image v0.43.0 // indirect - golang.org/x/net v0.56.0 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/text v0.38.0 // indirect - golang.org/x/time v0.15.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/image v0.46.0 // indirect + golang.org/x/net v0.59.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect + golang.org/x/time v0.16.0 // indirect + google.golang.org/protobuf v1.36.12 // 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 ) diff --git a/go.sum b/go.sum index f6e6183..7cd51cb 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= 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/go.mod h1:b3ueW9h3pGGXyhkormZAvilHaG4+mQti+bMNPdQBeOQ= 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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 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.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= -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-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY= +github.com/go-chi/chi/v5 v5.3.2/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/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.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= +github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME= +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/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= @@ -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/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 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.9.2/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/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg= +github.com/jackc/pgx/v5 v5.11.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/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 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/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/go.mod h1:imTtizoFtpfZqPqGP8vltVBB6q9yWcv6llBhfFeElZU= 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.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= -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/mattn/go-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U= +github.com/mattn/go-sqlite3 v1.14.52/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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= -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/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0= +github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4= 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/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/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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= -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/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ= +golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew= 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.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -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/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= +golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= 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.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -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/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= 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.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= -golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= -golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= +golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s= 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.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +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/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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= -gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/driver/postgres v1.6.2 h1:BvXQ/cNUg63q5TFNg672DmDcowZSFrNLkkA3Xe6GXq4= +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/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= -gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= -gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo= +gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/hooks/auth-check.go b/hooks/auth-check.go index 1b3e1e7..b3f8fab 100644 --- a/hooks/auth-check.go +++ b/hooks/auth-check.go @@ -3,8 +3,6 @@ package hooks import ( "errors" "net/http" - "os" - "strconv" "strings" "git.smarteching.com/goffee/core" @@ -14,90 +12,54 @@ import ( var CheckSessionCookie core.Hook = func(c *core.Context) { - pass := true - token := "" - usercookie, err := c.GetCookie() - if err != nil { + pass := false - } - token = usercookie.Token - if token == "" { - pass = false - } else { - payload, err := c.GetJWT().DecodeToken(token) - if err != nil { - pass = false + // Validate the cookie based session through the core session. This also + // transparently performs sliding expiration renewal when applicable. + session := c.GetSession() + if session.Init(c) { + // ensure the authenticated user still exists in the database + var user models.User + res := c.GetGorm().Where("id = ?", session.GetUserID()).First(&user) + if res.Error == nil { + pass = true + } else if errors.Is(res.Error, gorm.ErrRecordNotFound) { + // the user no longer exists: invalidate the session and remove the + // now-useless cookie so the client does not keep sending it. + _ = c.GetCache().Delete(core.CreateAuthTokenHashedCacheKey(session.GetUserID(), c.GetUserAgent())) + _ = core.ClearCookie(c.Response.HttpResponseWriter) } else { - - 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 - } - } + // database error: log it; the session is not validated. + c.GetLogger().Error(res.Error.Error()) + _ = core.ClearCookie(c.Response.HttpResponseWriter) } } - // if have session redirect protected page + if pass { - c.Response.Redirect("/appsample").ForceSendResponse() + c.Next() + } else { + c.Response.Redirect("/applogin").ForceSendResponse() 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 - TemplateEnableStr := os.Getenv("TEMPLATE_ENABLE") - if TemplateEnableStr == "" { - TemplateEnableStr = "false" - } - TemplateEnable, _ := strconv.ParseBool(TemplateEnableStr) - - token := "" - - 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 - } + // --- API/bearer flow --- + 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) if err != nil { - if TemplateEnable { - c.Response.Redirect("/applogin").ForceSendResponse() - } else { - c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{ - "message": "unauthorized", - })).ForceSendResponse() - } + c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{ + "message": "unauthorized", + })).ForceSendResponse() return } userAgent := c.GetUserAgent() @@ -106,24 +68,16 @@ var AuthCheck core.Hook = func(c *core.Context) { cachedToken, err := c.GetCache().Get(hashedCacheKey) if err != nil { // user signed out - if TemplateEnable { - c.Response.Redirect("/applogin").ForceSendResponse() - } else { - c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{ - "message": "unauthorized", - })).ForceSendResponse() - } + c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{ + "message": "unauthorized", + })).ForceSendResponse() return } if cachedToken != token { // using old token replaced with new one after recent signin - if TemplateEnable { - c.Response.Redirect("/applogin").ForceSendResponse() - } else { - c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{ - "message": "unauthorized", - })).ForceSendResponse() - } + c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{ + "message": "unauthorized", + })).ForceSendResponse() return } @@ -132,13 +86,9 @@ var AuthCheck core.Hook = func(c *core.Context) { if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { // error with the database c.GetLogger().Error(res.Error.Error()) - if TemplateEnable { - c.Response.Redirect("/applogin").ForceSendResponse() - } else { - c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]interface{}{ - "message": "internal error", - })).ForceSendResponse() - } + c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]interface{}{ + "message": "internal error", + })).ForceSendResponse() return } diff --git a/routes.go b/routes.go index e434b33..f4683b9 100644 --- a/routes.go +++ b/routes.go @@ -22,11 +22,14 @@ func registerRoutes() { controller.Get("/", controllers.WelcomeHome) // Uncomment the lines below to enable authentication API - controller.Post("/signup", controllers.Signup) - controller.Post("/signin", controllers.Signin) - controller.Post("/signout", controllers.Signout) - controller.Post("/reset-password", controllers.ResetPasswordRequest) - controller.Post("/reset-password/code/:code", controllers.SetNewPassword) + controller.Post("/api/signin", controllers.APISignin) + controller.Post("/api/signout", controllers.APISignout) + // API actions + controller.Post("/api/signup", controllers.APISignup) + 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 controller.Get("/queuesample", controllers.Queuesample) @@ -45,8 +48,12 @@ func registerRoutes() { controller.Post("/admin/users/delete", controllers.AdminUsersDelete) controller.Post("/admin/users/deleteconfirm", controllers.AdminUsersDelConfirm) - controller.Get("/dashboard", controllers.WelcomeToDashboard, hooks.AuthCheck) - controller.Get("/signout", controllers.Signout) - controller.Get("/applogin", controllers.AppLogin, hooks.CheckSessionCookie) - controller.Post("/applogin", controllers.AppLogin, hooks.CheckSessionCookie) + controller.Get("/appsignout", controllers.AppSignout, hooks.CheckSessionCookie) + controller.Get("/applogin", controllers.AppLogin) + controller.Post("/applogin", controllers.AppLogin) + controller.Post("/appsignin", controllers.AppSignin) + // Cookie protected route + controller.Get("/dashboard", controllers.WelcomeToDashboard, hooks.CheckSessionCookie) + controller.Post("/dashboard", controllers.WelcomeToDashboard, hooks.CheckSessionCookie) + } diff --git a/storage/templates/dashboard.html b/storage/templates/dashboard.html new file mode 100644 index 0000000..dacbf28 --- /dev/null +++ b/storage/templates/dashboard.html @@ -0,0 +1,15 @@ + + + {{template "page_head" "Dashhboard page"}} + +
+
+

Welcome to protected dashboard

+ +
+

Singout

+
+
+ {{template "page_footer"}} + + \ No newline at end of file diff --git a/storage/templates/login.html b/storage/templates/login.html index 96ce987..56f6690 100644 --- a/storage/templates/login.html +++ b/storage/templates/login.html @@ -4,7 +4,7 @@
-
+