Compare commits
No commits in common. "ec47943a390a3fa63a65068d8de6f18166fad981" and "6149d2945b1e0d193b272d63ecae4031b2cd2160" have entirely different histories.
ec47943a39
...
6149d2945b
8 changed files with 20 additions and 252 deletions
|
|
@ -24,8 +24,6 @@ type GormConfig struct {
|
||||||
|
|
||||||
type QueueConfig struct {
|
type QueueConfig struct {
|
||||||
EnableQueue bool
|
EnableQueue bool
|
||||||
Concurrency int
|
|
||||||
Queues map[string]int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CacheConfig struct {
|
type CacheConfig struct {
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ type Context struct {
|
||||||
GetMailer func() *Mailer
|
GetMailer func() *Mailer
|
||||||
GetEventsManager func() *EventsManager
|
GetEventsManager func() *EventsManager
|
||||||
GetLogger func() *logger.Logger
|
GetLogger func() *logger.Logger
|
||||||
GetSession func() *SessionUser
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO enhance
|
// TODO enhance
|
||||||
|
|
|
||||||
27
cookies.go
27
cookies.go
|
|
@ -19,7 +19,6 @@ 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.
|
||||||
|
|
@ -85,8 +84,9 @@ 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,36 +124,15 @@ 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: maxAge,
|
MaxAge: 3600,
|
||||||
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.
|
||||||
|
|
|
||||||
31
core.go
31
core.go
|
|
@ -26,7 +26,6 @@ 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
|
||||||
|
|
@ -277,7 +276,6 @@ func (app *App) makeHTTPRouterHandlerFunc(h Controller, ms []Hook) httprouter.Ha
|
||||||
GetMailer: resolveMailer(),
|
GetMailer: resolveMailer(),
|
||||||
GetEventsManager: resolveEventsManager(),
|
GetEventsManager: resolveEventsManager(),
|
||||||
GetLogger: resolveLogger(),
|
GetLogger: resolveLogger(),
|
||||||
GetSession: getSession(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.prepare(ctx)
|
ctx.prepare(ctx)
|
||||||
|
|
@ -309,11 +307,7 @@ 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 != "" {
|
||||||
statusCode := ctx.Response.redirectStatusCode
|
http.Redirect(w, r, ctx.Response.redirectTo, http.StatusTemporaryRedirect)
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
@ -523,18 +517,13 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -571,9 +560,7 @@ 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.
|
||||||
|
|
@ -594,9 +581,7 @@ 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.
|
||||||
|
|
@ -686,14 +671,6 @@ func resolveLogger() func() *logger.Logger {
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
// getSession returns a function that creates and returns a new SessionUser instance when invoked.
|
|
||||||
func getSession() func() *SessionUser {
|
|
||||||
f := func() *SessionUser {
|
|
||||||
return &SessionUser{}
|
|
||||||
}
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeDirs creates the specified directories under the base path with the provided permissions (0766).
|
// MakeDirs creates the specified directories under the base path with the provided permissions (0766).
|
||||||
func (app *App) MakeDirs(dirs ...string) {
|
func (app *App) MakeDirs(dirs ...string) {
|
||||||
o := syscall.Umask(0)
|
o := syscall.Umask(0)
|
||||||
|
|
|
||||||
28
queue.go
28
queue.go
|
|
@ -27,32 +27,22 @@ func (q *Queuemux) AddWork(pattern string, work Asynqtask) {
|
||||||
q.themux.HandleFunc(pattern, work)
|
q.themux.HandleFunc(pattern, work)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunQueueserver starts the queue server with the given configuration and handles tasks using the assigned ServeMux.
|
// RunQueueserver starts the queue server with predefined configurations and handles tasks using the assigned ServeMux.
|
||||||
// Configures the queue server with concurrency limits and priority-based queue management.
|
// Configures the queue server with concurrency limits and priority-based queue management.
|
||||||
// Logs and terminates the program if the server fails to run.
|
// Logs and terminates the program if the server fails to run.
|
||||||
func (q *Queuemux) RunQueueserver(config QueueConfig) {
|
func (q *Queuemux) RunQueueserver() {
|
||||||
|
|
||||||
redisAddr := fmt.Sprintf("%v:%v", os.Getenv("REDIS_HOST"), os.Getenv("REDIS_PORT"))
|
redisAddr := fmt.Sprintf("%v:%v", os.Getenv("REDIS_HOST"), os.Getenv("REDIS_PORT"))
|
||||||
|
|
||||||
queues := config.Queues
|
|
||||||
if queues == nil {
|
|
||||||
queues = map[string]int{
|
|
||||||
"critical": 6,
|
|
||||||
"default": 3,
|
|
||||||
"low": 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
concurrency := config.Concurrency
|
|
||||||
if concurrency <= 0 {
|
|
||||||
concurrency = 10
|
|
||||||
}
|
|
||||||
|
|
||||||
srv := asynq.NewServer(
|
srv := asynq.NewServer(
|
||||||
asynq.RedisClientOpt{Addr: redisAddr},
|
asynq.RedisClientOpt{Addr: redisAddr},
|
||||||
asynq.Config{
|
asynq.Config{Concurrency: 10,
|
||||||
Concurrency: concurrency,
|
// Optionally specify multiple queues with different priority.
|
||||||
Queues: queues,
|
Queues: map[string]int{
|
||||||
|
"critical": 6,
|
||||||
|
"default": 3,
|
||||||
|
"low": 1,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
17
response.go
17
response.go
|
|
@ -20,7 +20,6 @@ type Response struct {
|
||||||
overrideContentType string
|
overrideContentType string
|
||||||
isTerminated bool
|
isTerminated bool
|
||||||
redirectTo string
|
redirectTo string
|
||||||
redirectStatusCode int
|
|
||||||
HttpResponseWriter http.ResponseWriter
|
HttpResponseWriter http.ResponseWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,7 +96,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
|
||||||
rs.body = buffer.Bytes()
|
buffer.WriteTo(rs.HttpResponseWriter)
|
||||||
}
|
}
|
||||||
return rs
|
return rs
|
||||||
}
|
}
|
||||||
|
|
@ -137,23 +136,14 @@ func (rs *Response) ForceSendResponse() {
|
||||||
rs.isTerminated = true
|
rs.isTerminated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect sends a redirect response to the given URL.
|
// updates the redirect URL for the response and returns the modified Response. Validates the URL before setting it.
|
||||||
// By default it uses 307 (Temporary Redirect) to preserve the HTTP method.
|
func (rs *Response) Redirect(url string) *Response {
|
||||||
// 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
|
||||||
|
|
@ -163,7 +153,6 @@ func (rs *Response) Redirect(url string, use303 ...bool) *Response {
|
||||||
return rs
|
return rs
|
||||||
}
|
}
|
||||||
rs.redirectTo = url
|
rs.redirectTo = url
|
||||||
|
|
||||||
return rs
|
return rs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
146
session.go
146
session.go
|
|
@ -1,146 +0,0 @@
|
||||||
// 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 core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SessionUser handles user session data management with thread-safe operations.
|
|
||||||
// Sessions are stored in the cache (Redis) and are tied to an authenticated user
|
|
||||||
// via JWT tokens stored in cookies.
|
|
||||||
type SessionUser struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
context *Context
|
|
||||||
userID uint
|
|
||||||
hashedSessionKey string
|
|
||||||
authenticated bool
|
|
||||||
sessionStart time.Time
|
|
||||||
values map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init initializes the session by validating the user's JWT token from the cookie.
|
|
||||||
// It checks the cached token, verifies it matches, and loads any existing session data.
|
|
||||||
// Returns true if the session was successfully established.
|
|
||||||
func (s *SessionUser) Init(c *Context) bool {
|
|
||||||
s.context = c
|
|
||||||
|
|
||||||
// get cookie
|
|
||||||
usercookie, err := c.GetCookie()
|
|
||||||
if err != nil || usercookie.Token == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
payload, err := c.GetJWT().DecodeToken(usercookie.Token)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
userID := uint(c.CastToInt(payload["userID"]))
|
|
||||||
userAgent := c.GetUserAgent()
|
|
||||||
|
|
||||||
// verify token against cached value
|
|
||||||
hashedCacheKey := CreateAuthTokenHashedCacheKey(userID, userAgent)
|
|
||||||
cachedToken, err := c.GetCache().Get(hashedCacheKey)
|
|
||||||
if err != nil || cachedToken != usercookie.Token {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// session established - load session data from cache
|
|
||||||
userAgent = c.GetUserAgent()
|
|
||||||
sessionKey := fmt.Sprintf("sess_%v", userAgent)
|
|
||||||
s.hashedSessionKey = CreateAuthTokenHashedCacheKey(userID, sessionKey)
|
|
||||||
s.values = make(map[string]interface{})
|
|
||||||
s.authenticated = true
|
|
||||||
s.userID = userID
|
|
||||||
|
|
||||||
value, _ := c.GetCache().Get(s.hashedSessionKey)
|
|
||||||
if len(value) > 0 {
|
|
||||||
_ = json.Unmarshal([]byte(value), &s.values)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set stores a value in the session and persists it to the cache.
|
|
||||||
func (s *SessionUser) Set(key string, value interface{}) error {
|
|
||||||
s.mu.Lock()
|
|
||||||
s.values[key] = value
|
|
||||||
s.mu.Unlock()
|
|
||||||
return s.Save()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get retrieves a value from the session. Returns the value and a boolean indicating if the key exists.
|
|
||||||
func (s *SessionUser) Get(key string) (interface{}, bool) {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
val, ok := s.values[key]
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes a specific key from the session and persists the change.
|
|
||||||
// Returns the deleted value if it existed.
|
|
||||||
func (s *SessionUser) Delete(key string) interface{} {
|
|
||||||
s.mu.RLock()
|
|
||||||
v, ok := s.values[key]
|
|
||||||
s.mu.RUnlock()
|
|
||||||
if ok {
|
|
||||||
s.mu.Lock()
|
|
||||||
delete(s.values, key)
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
|
||||||
s.Save()
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush deletes all session data from the cache.
|
|
||||||
func (s *SessionUser) Flush() error {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
if s.hashedSessionKey != "" {
|
|
||||||
_ = s.context.GetCache().Delete(s.hashedSessionKey)
|
|
||||||
}
|
|
||||||
s.values = make(map[string]interface{})
|
|
||||||
s.authenticated = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save persists the current session values to the cache.
|
|
||||||
func (s *SessionUser) Save() error {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
|
|
||||||
if len(s.values) > 0 {
|
|
||||||
buf, err := json.Marshal(&s.values)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.context.GetCache().Set(s.hashedSessionKey, string(buf))
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = s.context.GetCache().Delete(s.hashedSessionKey)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserID returns the user ID of the authenticated session user.
|
|
||||||
func (s *SessionUser) GetUserID() uint {
|
|
||||||
return s.userID
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsAuthenticated returns whether the session has been successfully authenticated.
|
|
||||||
func (s *SessionUser) IsAuthenticated() bool {
|
|
||||||
return s.authenticated
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateAuthTokenHashedCacheKey generates a hashed cache key used to store JWT tokens and session data.
|
|
||||||
// The key is based on the user ID and user agent, hashed with MD5.
|
|
||||||
func CreateAuthTokenHashedCacheKey(userID uint, userAgent string) string {
|
|
||||||
cacheKey := fmt.Sprintf("userid:_%v_useragent:_%v_jwt_token", userID, userAgent)
|
|
||||||
return fmt.Sprintf("%x", md5.Sum([]byte(cacheKey)))
|
|
||||||
}
|
|
||||||
20
templates.go
20
templates.go
|
|
@ -359,22 +359,4 @@ 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
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue