diff --git a/config.go b/config.go index 921cb24..c481de2 100644 --- a/config.go +++ b/config.go @@ -24,8 +24,6 @@ type GormConfig struct { type QueueConfig struct { EnableQueue bool - Concurrency int - Queues map[string]int } type CacheConfig struct { diff --git a/context.go b/context.go index 028ac5a..9015159 100644 --- a/context.go +++ b/context.go @@ -34,7 +34,6 @@ type Context struct { GetMailer func() *Mailer GetEventsManager func() *EventsManager GetLogger func() *logger.Logger - GetSession func() *SessionUser } // TODO enhance diff --git a/cookies.go b/cookies.go index 9830099..4e51470 100644 --- a/cookies.go +++ b/cookies.go @@ -19,7 +19,6 @@ import ( "os" "strconv" "strings" - "time" ) // 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. -// 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 { + // Initialize a User struct containing the data that we want to store in the + // cookie. var err error // check if template engine is enable @@ -124,36 +124,15 @@ func SetCookie(w http.ResponseWriter, email string, token string) error { 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 // the cookie value. cookie := http.Cookie{ Name: "goffee", Value: buf.String(), Path: "/", - MaxAge: maxAge, - Expires: time.Now().Add(time.Duration(maxAge) * time.Second), + MaxAge: 3600, HttpOnly: true, SameSite: http.SameSiteLaxMode, - Secure: cookieSecure, } // Write an encrypted cookie containing the gob-encoded data as normal. diff --git a/core.go b/core.go index 3af6adf..3846432 100644 --- a/core.go +++ b/core.go @@ -26,7 +26,6 @@ import ( "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" - gormlogger "gorm.io/gorm/logger" ) var loggr *logger.Logger @@ -277,7 +276,6 @@ func (app *App) makeHTTPRouterHandlerFunc(h Controller, ms []Hook) httprouter.Ha GetMailer: resolveMailer(), GetEventsManager: resolveEventsManager(), GetLogger: resolveLogger(), - GetSession: getSession(), } ctx.prepare(ctx) @@ -309,11 +307,7 @@ func (app *App) makeHTTPRouterHandlerFunc(h Controller, ms []Hook) httprouter.Ha w.WriteHeader(ctx.Response.statusCode) } if ctx.Response.redirectTo != "" { - statusCode := ctx.Response.redirectStatusCode - if statusCode == 0 { - statusCode = http.StatusTemporaryRedirect // default to 307 - } - http.Redirect(w, r, ctx.Response.redirectTo, statusCode) + http.Redirect(w, r, ctx.Response.redirectTo, http.StatusTemporaryRedirect) } else { w.Write(ctx.Response.body) } @@ -523,18 +517,13 @@ func NewGorm() *gorm.DB { if err != nil { panic(fmt.Sprintf("error locating sqlite file: %v", err.Error())) } - db, err = gorm.Open(sqlite.Open(fullSqlitePath), &gorm.Config{ - Logger: gormlogger.Default.LogMode(gormlogger.Silent), - }) + db, err = gorm.Open(sqlite.Open(fullSqlitePath), &gorm.Config{}) default: panic("database driver not selected") } 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)) } - if db != nil { - db.Logger = db.Logger.LogMode(gormlogger.Silent) - } return db } @@ -571,9 +560,7 @@ func postgresConnect() (*gorm.DB, error) { os.Getenv("POSTGRES_SSL_MODE"), os.Getenv("POSTGRES_TIMEZONE"), ) - return gorm.Open(postgres.Open(dsn), &gorm.Config{ - Logger: gormlogger.Default.LogMode(gormlogger.Silent), - }) + return gorm.Open(postgres.Open(dsn), &gorm.Config{}) } // 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 DontSupportRenameColumn: true, // `change` when rename column, rename column not supported before MySQL 8, MariaDB SkipInitializeWithVersion: false, // auto configure based on currently MySQL version - }), &gorm.Config{ - Logger: gormlogger.Default.LogMode(gormlogger.Silent), - }) + }), &gorm.Config{}) } // 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 } -// 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). func (app *App) MakeDirs(dirs ...string) { o := syscall.Umask(0) diff --git a/queue.go b/queue.go index 70de9cd..905a6ed 100644 --- a/queue.go +++ b/queue.go @@ -27,32 +27,22 @@ func (q *Queuemux) AddWork(pattern string, work Asynqtask) { 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. // 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")) - 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( asynq.RedisClientOpt{Addr: redisAddr}, - asynq.Config{ - Concurrency: concurrency, - Queues: queues, + asynq.Config{Concurrency: 10, + // Optionally specify multiple queues with different priority. + Queues: map[string]int{ + "critical": 6, + "default": 3, + "low": 1, + }, }, ) diff --git a/response.go b/response.go index 12b6406..d6202c7 100644 --- a/response.go +++ b/response.go @@ -20,7 +20,6 @@ type Response struct { overrideContentType string isTerminated bool redirectTo string - redirectStatusCode int HttpResponseWriter http.ResponseWriter } @@ -97,7 +96,7 @@ func (rs *Response) Template(name string, data interface{}) *Response { panic(fmt.Sprintf("error executing template: %v", err)) } rs.contentType = CONTENT_TYPE_HTML - rs.body = buffer.Bytes() + buffer.WriteTo(rs.HttpResponseWriter) } return rs } @@ -137,23 +136,14 @@ func (rs *Response) ForceSendResponse() { rs.isTerminated = true } -// Redirect sends a redirect response to the given URL. -// 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 { +// updates the redirect URL for the response and returns the modified Response. Validates the URL before setting it. +func (rs *Response) Redirect(url string) *Response { validator := resolveValidator() v := validator.Validate(map[string]interface{}{ "url": url, }, map[string]interface{}{ "url": "url", }) - - if len(use303) > 0 && use303[0] { - rs.redirectStatusCode = http.StatusSeeOther // 303 - } else { - rs.redirectStatusCode = http.StatusTemporaryRedirect // 307 (default) - } - if v.Failed() { if url[0:1] != "/" { rs.redirectTo = "/" + url @@ -163,7 +153,6 @@ func (rs *Response) Redirect(url string, use303 ...bool) *Response { return rs } rs.redirectTo = url - return rs } diff --git a/session.go b/session.go deleted file mode 100644 index 16888de..0000000 --- a/session.go +++ /dev/null @@ -1,146 +0,0 @@ -// 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 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))) -} diff --git a/templates.go b/templates.go index cf6692e..4ee0050 100644 --- a/templates.go +++ b/templates.go @@ -359,22 +359,4 @@ func NewTemplates(components embed.FS, templates embed.FS) { ParseFS(components, paths...), ) 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 -} +} \ No newline at end of file