package session import ( "fmt" "net/http" "time" "git.kirsle.net/apps/gophertype/pkg/cache" "git.kirsle.net/apps/gophertype/pkg/console" "git.kirsle.net/apps/gophertype/pkg/constants" "github.com/google/uuid" ) // Session cookie object that is kept server side in Redis. type Session struct { UUID string `json:"-"` // not stored LoggedIn bool `json:"loggedIn"` UserID int `json:"userId,omitempty"` Flashes []string `json:"flashes,omitempty"` Errors []string `json:"errors,omitempty"` AgeOK bool `json:"ageOK,omitempty"` EditToken string `json:"editToken,omitempty"` Email string `json:"email,omitempty"` Name string `json:"name,omitempty"` LastSeen time.Time `json:"lastSeen"` } const ( ContextKey = "session" CurrentUserKey = "current_user" CSRFKey = "csrf" RequestTimeKey = "req_time" ) // New creates a blank session object. func New() *Session { return &Session{ UUID: uuid.New().String(), Flashes: []string{}, Errors: []string{}, } } // Load the session from the browser session_id token and Redis or creates a new session. func LoadOrNew(r *http.Request) *Session { var sess = New() // Read the session cookie value. cookie, err := r.Cookie(constants.SessionCookieName) if err != nil { console.Debug("session.LoadOrNew: cookie error, new sess: %s", err) return sess } // Look up this UUID in Redis. sess.UUID = cookie.Value key := fmt.Sprintf(constants.SessionRedisKeyFormat, sess.UUID) err = cache.GetJSON(key, sess) // console.Error("LoadOrNew: raw from Redis: %+v", sess) if err != nil { console.Error("session.LoadOrNew: didn't find %s in Redis: %s", key, err) } return sess } // Save the session and send a cookie header. func (s *Session) Save(w http.ResponseWriter) { // Roll a UUID session_id value. if s.UUID == "" { s.UUID = uuid.New().String() } // Ensure it is a valid UUID. if _, err := uuid.Parse(s.UUID); err != nil { console.Error("Session.Save: got an invalid UUID session_id: %s", err) s.UUID = uuid.New().String() } // Ping last seen. s.LastSeen = time.Now() // Save their session object in Redis. key := fmt.Sprintf(constants.SessionRedisKeyFormat, s.UUID) if err := cache.SetJSON(key, s, constants.SessionCookieMaxAge*time.Second); err != nil { console.Error("Session.Save: couldn't write to Redis: %s", err) } cookie := &http.Cookie{ Name: constants.SessionCookieName, Value: s.UUID, MaxAge: constants.SessionCookieMaxAge, Path: "/", HttpOnly: true, } http.SetCookie(w, cookie) } // Get the session from the current HTTP request context. func Get(r *http.Request) *Session { if r == nil { panic("session.Get: http.Request is required") } // Cached in the request context? ctx := r.Context() if sess, ok := ctx.Value(ContextKey).(*Session); ok { return sess } return LoadOrNew(r) } // ReadFlashes returns and clears the Flashes and Errors for this session. func (s *Session) ReadFlashes(w http.ResponseWriter) (flashes, errors []string) { flashes = s.Flashes errors = s.Errors s.Flashes = []string{} s.Errors = []string{} if len(flashes)+len(errors) > 0 { s.Save(w) } return flashes, errors } // Flash adds a transient message to the user's session to show on next page load. func Flash(w http.ResponseWriter, r *http.Request, msg string, args ...interface{}) { sess := Get(r) sess.Flashes = append(sess.Flashes, fmt.Sprintf(msg, args...)) sess.Save(w) } // FlashError adds a transient error message to the session. func FlashError(w http.ResponseWriter, r *http.Request, msg string, args ...interface{}) { sess := Get(r) sess.Errors = append(sess.Errors, fmt.Sprintf(msg, args...)) sess.Save(w) } // LoginUser marks a session as logged in to an account. func LoginUser(w http.ResponseWriter, r *http.Request, userID int) error { sess := Get(r) sess.LoggedIn = true sess.UserID = userID sess.Save(w) return nil } // LogoutUser signs a user out. func LogoutUser(w http.ResponseWriter, r *http.Request) { sess := Get(r) sess.LoggedIn = false sess.UserID = 0 sess.Save(w) }