package session import ( "encoding/base64" "fmt" "log/slog" "net/http" "github.com/gorilla/sessions" "go.uber.org/fx" "sneak.berlin/go/webhooker/internal/config" "sneak.berlin/go/webhooker/internal/logger" ) const ( // SessionName is the name of the session cookie SessionName = "webhooker_session" // UserIDKey is the session key for user ID UserIDKey = "user_id" // UsernameKey is the session key for username UsernameKey = "username" // AuthenticatedKey is the session key for authentication status AuthenticatedKey = "authenticated" ) // nolint:revive // SessionParams is a standard fx naming convention type SessionParams struct { fx.In Config *config.Config Logger *logger.Logger } // Session manages encrypted session storage type Session struct { store *sessions.CookieStore log *slog.Logger config *config.Config } // New creates a new session manager func New(lc fx.Lifecycle, params SessionParams) (*Session, error) { if params.Config.SessionKey == "" { return nil, fmt.Errorf("SESSION_KEY environment variable is required") } // Decode the base64 session key keyBytes, err := base64.StdEncoding.DecodeString(params.Config.SessionKey) if err != nil { return nil, fmt.Errorf("invalid SESSION_KEY format: %w", err) } if len(keyBytes) != 32 { return nil, fmt.Errorf("SESSION_KEY must be 32 bytes (got %d)", len(keyBytes)) } store := sessions.NewCookieStore(keyBytes) // Configure cookie options for security store.Options = &sessions.Options{ Path: "/", MaxAge: 86400 * 7, // 7 days HttpOnly: true, Secure: !params.Config.IsDev(), // HTTPS in production SameSite: http.SameSiteLaxMode, } s := &Session{ store: store, log: params.Logger.Get(), config: params.Config, } return s, nil } // Get retrieves a session for the request func (s *Session) Get(r *http.Request) (*sessions.Session, error) { return s.store.Get(r, SessionName) } // Save saves the session func (s *Session) Save(r *http.Request, w http.ResponseWriter, sess *sessions.Session) error { return sess.Save(r, w) } // SetUser sets the user information in the session func (s *Session) SetUser(sess *sessions.Session, userID, username string) { sess.Values[UserIDKey] = userID sess.Values[UsernameKey] = username sess.Values[AuthenticatedKey] = true } // ClearUser removes user information from the session func (s *Session) ClearUser(sess *sessions.Session) { delete(sess.Values, UserIDKey) delete(sess.Values, UsernameKey) delete(sess.Values, AuthenticatedKey) } // IsAuthenticated checks if the session has an authenticated user func (s *Session) IsAuthenticated(sess *sessions.Session) bool { auth, ok := sess.Values[AuthenticatedKey].(bool) return ok && auth } // GetUserID retrieves the user ID from the session func (s *Session) GetUserID(sess *sessions.Session) (string, bool) { userID, ok := sess.Values[UserIDKey].(string) return userID, ok } // GetUsername retrieves the username from the session func (s *Session) GetUsername(sess *sessions.Session) (string, bool) { username, ok := sess.Values[UsernameKey].(string) return username, ok } // Destroy invalidates the session func (s *Session) Destroy(sess *sessions.Session) { sess.Options.MaxAge = -1 s.ClearUser(sess) }