Compare commits

...
Sign in to create a new pull request.

7 commits

4 changed files with 75 additions and 11 deletions

View file

@ -19,6 +19,7 @@ import (
"os" "os"
"strconv" "strconv"
"strings" "strings"
"time"
) )
// ErrValueTooLong indicates that the cookie value exceeds the allowed length limit. // ErrValueTooLong indicates that the cookie value exceeds the allowed length limit.
@ -84,9 +85,8 @@ func GetCookie(r *http.Request) (UserCookie, error) {
} }
// SetCookie sets an encrypted cookie with a user's email and token, using gob encoding for data serialization. // SetCookie sets an encrypted cookie with a user's email and token, using gob encoding for data serialization.
// The Secure flag is controlled by the COOKIE_SECURE environment variable (defaults to true, set to false for local HTTP development).
func SetCookie(w http.ResponseWriter, email string, token string) error { func SetCookie(w http.ResponseWriter, email string, token string) error {
// Initialize a User struct containing the data that we want to store in the
// cookie.
var err error var err error
// check if template engine is enable // check if template engine is enable
@ -124,15 +124,36 @@ func SetCookie(w http.ResponseWriter, email string, token string) error {
return err return err
} }
// Derive cookie MaxAge from JWT_LIFESPAN_MINUTES (default: 1440 min = 1 day)
maxAge := 1440 * 60 // default 1 day in seconds
lifetimeStr := os.Getenv("JWT_LIFESPAN_MINUTES")
if lifetimeStr != "" {
lifetime, parseErr := strconv.Atoi(lifetimeStr)
if parseErr == nil {
maxAge = lifetime * 60 // convert minutes to seconds
}
}
// Determine if the cookie should have the Secure flag.
// Set COOKIE_SECURE=false (or "0", "f") in your .env for local development over HTTP.
// Defaults to true for production safety.
cookieSecureStr := os.Getenv("COOKIE_SECURE")
if cookieSecureStr == "" {
cookieSecureStr = "true"
}
cookieSecure, _ := strconv.ParseBool(cookieSecureStr)
// Call buf.String() to get the gob-encoded value as a string and set it as // Call buf.String() to get the gob-encoded value as a string and set it as
// the cookie value. // the cookie value.
cookie := http.Cookie{ cookie := http.Cookie{
Name: "goffee", Name: "goffee",
Value: buf.String(), Value: buf.String(),
Path: "/", Path: "/",
MaxAge: 3600, MaxAge: maxAge,
Expires: time.Now().Add(time.Duration(maxAge) * time.Second),
HttpOnly: true, HttpOnly: true,
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
Secure: cookieSecure,
} }
// Write an encrypted cookie containing the gob-encoded data as normal. // Write an encrypted cookie containing the gob-encoded data as normal.

22
core.go
View file

@ -26,6 +26,7 @@ import (
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
gormlogger "gorm.io/gorm/logger"
) )
var loggr *logger.Logger var loggr *logger.Logger
@ -307,7 +308,11 @@ func (app *App) makeHTTPRouterHandlerFunc(h Controller, ms []Hook) httprouter.Ha
w.WriteHeader(ctx.Response.statusCode) w.WriteHeader(ctx.Response.statusCode)
} }
if ctx.Response.redirectTo != "" { if ctx.Response.redirectTo != "" {
http.Redirect(w, r, ctx.Response.redirectTo, http.StatusTemporaryRedirect) statusCode := ctx.Response.redirectStatusCode
if statusCode == 0 {
statusCode = http.StatusTemporaryRedirect // default to 307
}
http.Redirect(w, r, ctx.Response.redirectTo, statusCode)
} else { } else {
w.Write(ctx.Response.body) w.Write(ctx.Response.body)
} }
@ -517,13 +522,18 @@ func NewGorm() *gorm.DB {
if err != nil { if err != nil {
panic(fmt.Sprintf("error locating sqlite file: %v", err.Error())) panic(fmt.Sprintf("error locating sqlite file: %v", err.Error()))
} }
db, err = gorm.Open(sqlite.Open(fullSqlitePath), &gorm.Config{}) db, err = gorm.Open(sqlite.Open(fullSqlitePath), &gorm.Config{
Logger: gormlogger.Default.LogMode(gormlogger.Silent),
})
default: default:
panic("database driver not selected") panic("database driver not selected")
} }
if gormC.EnableGorm && err != nil { if gormC.EnableGorm && err != nil {
panic(fmt.Sprintf("gorm has problem connecting to %v, (if it's not needed you can disable it in config/gorm.go): %v", os.Getenv("DB_DRIVER"), err)) panic(fmt.Sprintf("gorm has problem connecting to %v, (if it's not needed you can disable it in config/gorm.go): %v", os.Getenv("DB_DRIVER"), err))
} }
if db != nil {
db.Logger = db.Logger.LogMode(gormlogger.Silent)
}
return db return db
} }
@ -560,7 +570,9 @@ func postgresConnect() (*gorm.DB, error) {
os.Getenv("POSTGRES_SSL_MODE"), os.Getenv("POSTGRES_SSL_MODE"),
os.Getenv("POSTGRES_TIMEZONE"), os.Getenv("POSTGRES_TIMEZONE"),
) )
return gorm.Open(postgres.Open(dsn), &gorm.Config{}) return gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: gormlogger.Default.LogMode(gormlogger.Silent),
})
} }
// mysqlConnect establishes a connection to a MySQL database using credentials and configurations from environment variables. // mysqlConnect establishes a connection to a MySQL database using credentials and configurations from environment variables.
@ -581,7 +593,9 @@ func mysqlConnect() (*gorm.DB, error) {
DontSupportRenameIndex: true, // drop & create when rename index, rename index not supported before MySQL 5.7, MariaDB DontSupportRenameIndex: true, // drop & create when rename index, rename index not supported before MySQL 5.7, MariaDB
DontSupportRenameColumn: true, // `change` when rename column, rename column not supported before MySQL 8, MariaDB DontSupportRenameColumn: true, // `change` when rename column, rename column not supported before MySQL 8, MariaDB
SkipInitializeWithVersion: false, // auto configure based on currently MySQL version SkipInitializeWithVersion: false, // auto configure based on currently MySQL version
}), &gorm.Config{}) }), &gorm.Config{
Logger: gormlogger.Default.LogMode(gormlogger.Silent),
})
} }
// getJWT returns a function that initializes and provides a *JWT instance configured with environment variables. // getJWT returns a function that initializes and provides a *JWT instance configured with environment variables.

View file

@ -20,6 +20,7 @@ type Response struct {
overrideContentType string overrideContentType string
isTerminated bool isTerminated bool
redirectTo string redirectTo string
redirectStatusCode int
HttpResponseWriter http.ResponseWriter HttpResponseWriter http.ResponseWriter
} }
@ -96,7 +97,7 @@ func (rs *Response) Template(name string, data interface{}) *Response {
panic(fmt.Sprintf("error executing template: %v", err)) panic(fmt.Sprintf("error executing template: %v", err))
} }
rs.contentType = CONTENT_TYPE_HTML rs.contentType = CONTENT_TYPE_HTML
buffer.WriteTo(rs.HttpResponseWriter) rs.body = buffer.Bytes()
} }
return rs return rs
} }
@ -136,14 +137,23 @@ func (rs *Response) ForceSendResponse() {
rs.isTerminated = true rs.isTerminated = true
} }
// updates the redirect URL for the response and returns the modified Response. Validates the URL before setting it. // Redirect sends a redirect response to the given URL.
func (rs *Response) Redirect(url string) *Response { // By default it uses 307 (Temporary Redirect) to preserve the HTTP method.
// Pass true as the second argument to use 303 (See Other), which changes POST to GET.
func (rs *Response) Redirect(url string, use303 ...bool) *Response {
validator := resolveValidator() validator := resolveValidator()
v := validator.Validate(map[string]interface{}{ v := validator.Validate(map[string]interface{}{
"url": url, "url": url,
}, map[string]interface{}{ }, map[string]interface{}{
"url": "url", "url": "url",
}) })
if len(use303) > 0 && use303[0] {
rs.redirectStatusCode = http.StatusSeeOther // 303
} else {
rs.redirectStatusCode = http.StatusTemporaryRedirect // 307 (default)
}
if v.Failed() { if v.Failed() {
if url[0:1] != "/" { if url[0:1] != "/" {
rs.redirectTo = "/" + url rs.redirectTo = "/" + url
@ -153,6 +163,7 @@ func (rs *Response) Redirect(url string) *Response {
return rs return rs
} }
rs.redirectTo = url rs.redirectTo = url
return rs return rs
} }

View file

@ -359,4 +359,22 @@ func NewTemplates(components embed.FS, templates embed.FS) {
ParseFS(components, paths...), ParseFS(components, paths...),
) )
tmpl = template.Must(tmpl.ParseFS(templates, pathst...)) tmpl = template.Must(tmpl.ParseFS(templates, pathst...))
} }
// RenderNamedTemplate executes a named template from the registered set with
// the given data and returns the rendered HTML.
// Usage:
//
// html, err := core.RenderNamedTemplate("tabler_table", data)
// if err != nil {
// // handle error
// }
// return c.Response.HTML(string(html))
func RenderNamedTemplate(name string, data interface{}) (template.HTML, error) {
var buf strings.Builder
if err := tmpl.ExecuteTemplate(&buf, name, data); err != nil {
return "", fmt.Errorf("failed to execute template %q: %w", name, err)
}
return template.HTML(buf.String()), nil
}