package auth import ( "context" "crypto/rand" "crypto/subtle" "database/sql" "encoding/hex" "net/http" "time" "github.com/alexedwards/scs/v2" ) type contextKey string const ctxUser contextKey = "user" // User is the authenticated user stored in request context. type User struct { ID int64 Name string } // SQLiteStore implements scs.Store using a modernc.org/sqlite connection. type SQLiteStore struct { db *sql.DB } func NewStore(db *sql.DB) *SQLiteStore { return &SQLiteStore{db: db} } func (s *SQLiteStore) Delete(token string) error { _, err := s.db.Exec("DELETE FROM sessions WHERE token = ?", token) return err } func (s *SQLiteStore) Find(token string) ([]byte, bool, error) { var data []byte err := s.db.QueryRow( "SELECT data FROM sessions WHERE token = ? AND expiry > ?", token, time.Now().Unix(), ).Scan(&data) if err == sql.ErrNoRows { return nil, false, nil } if err != nil { return nil, false, err } return data, true, nil } func (s *SQLiteStore) Commit(token string, b []byte, expiry time.Time) error { _, err := s.db.Exec( "INSERT OR REPLACE INTO sessions (token, data, expiry) VALUES (?, ?, ?)", token, b, expiry.Unix(), ) return err } // Manager wraps scs.SessionManager with application-level helpers. type Manager struct { SM *scs.SessionManager } func NewManager(db *sql.DB) *Manager { sm := scs.New() sm.Store = NewStore(db) sm.Lifetime = 30 * 24 * time.Hour sm.Cookie.HttpOnly = true sm.Cookie.SameSite = http.SameSiteLaxMode return &Manager{SM: sm} } // SetUser persists the authenticated user in the session. func (m *Manager) SetUser(r *http.Request, id int64, name string) { m.SM.Put(r.Context(), "userID", id) m.SM.Put(r.Context(), "userName", name) } // ClearUser destroys the session (used on logout). func (m *Manager) ClearUser(r *http.Request) error { return m.SM.Destroy(r.Context()) } // UserFromCtx returns the authenticated user from context, or nil. func UserFromCtx(ctx context.Context) *User { u, _ := ctx.Value(ctxUser).(*User) return u } // RequireAuth redirects unauthenticated requests to /login. func (m *Manager) RequireAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := m.SM.GetInt64(r.Context(), "userID") if id == 0 { http.Redirect(w, r, "/login", http.StatusSeeOther) return } name := m.SM.GetString(r.Context(), "userName") ctx := context.WithValue(r.Context(), ctxUser, &User{ID: id, Name: name}) next.ServeHTTP(w, r.WithContext(ctx)) }) } // CSRFToken returns the session's CSRF token, creating one if needed. func (m *Manager) CSRFToken(r *http.Request) string { tok := m.SM.GetString(r.Context(), "csrf") if tok != "" { return tok } b := make([]byte, 16) rand.Read(b) tok = hex.EncodeToString(b) m.SM.Put(r.Context(), "csrf", tok) return tok } // CheckCSRF returns true if the form's csrf_token matches the session token. func (m *Manager) CheckCSRF(r *http.Request) bool { session := m.SM.GetString(r.Context(), "csrf") form := r.FormValue("csrf_token") return session != "" && subtle.ConstantTimeCompare([]byte(session), []byte(form)) == 1 } // SetFlash stores a one-time flash message in the session. func (m *Manager) SetFlash(r *http.Request, msg string) { m.SM.Put(r.Context(), "flash", msg) } // PopFlash reads and removes the flash message. func (m *Manager) PopFlash(r *http.Request) string { return m.SM.PopString(r.Context(), "flash") }