Session middleware
This commit is contained in:
parent
25160ef847
commit
18a9096684
10 changed files with 191 additions and 13 deletions
8
context.go
Normal file
8
context.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
type ctxKey struct{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
requestIDKey ctxKey
|
||||||
|
sessionKey ctxKey
|
||||||
|
)
|
|
@ -15,9 +15,7 @@ func Logger(logger *slog.Logger) Middleware {
|
||||||
slog.String("proto", r.Proto),
|
slog.String("proto", r.Proto),
|
||||||
slog.String("method", r.Method),
|
slog.String("method", r.Method),
|
||||||
slog.String("request_uri", r.RequestURI),
|
slog.String("request_uri", r.RequestURI),
|
||||||
}
|
slog.String("request_id", requestID),
|
||||||
if requestID != "" {
|
|
||||||
args = append(args, slog.String("request_id", requestID))
|
|
||||||
}
|
}
|
||||||
logger.InfoContext(
|
logger.InfoContext(
|
||||||
r.Context(),
|
r.Context(),
|
||||||
|
|
|
@ -2,7 +2,6 @@ package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime/debug"
|
|
||||||
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
)
|
)
|
||||||
|
@ -15,7 +14,6 @@ func Recover(logger *slog.Logger) Middleware {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
debug.PrintStack()
|
|
||||||
requestID := GetRequestID(r)
|
requestID := GetRequestID(r)
|
||||||
logger.ErrorContext(
|
logger.ErrorContext(
|
||||||
r.Context(),
|
r.Context(),
|
||||||
|
|
|
@ -7,12 +7,7 @@ import (
|
||||||
"go.neonxp.ru/objectid"
|
"go.neonxp.ru/objectid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ctxKeyRequestID int
|
const RequestIDHeader string = "X-Request-ID"
|
||||||
|
|
||||||
const (
|
|
||||||
RequestIDKey ctxKeyRequestID = 0
|
|
||||||
RequestIDHeader string = "X-Request-ID"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RequestID(next http.Handler) http.Handler {
|
func RequestID(next http.Handler) http.Handler {
|
||||||
objectid.Seed()
|
objectid.Seed()
|
||||||
|
@ -22,12 +17,12 @@ func RequestID(next http.Handler) http.Handler {
|
||||||
requestID = objectid.New().String()
|
requestID = objectid.New().String()
|
||||||
}
|
}
|
||||||
|
|
||||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), RequestIDKey, requestID)))
|
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), requestIDKey, requestID)))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRequestID(r *http.Request) string {
|
func GetRequestID(r *http.Request) string {
|
||||||
rid := r.Context().Value(RequestIDKey)
|
rid := r.Context().Value(requestIDKey)
|
||||||
if rid == nil {
|
if rid == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
65
session.go
Normal file
65
session.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.neonxp.ru/middleware/session"
|
||||||
|
"go.neonxp.ru/objectid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionConfig struct {
|
||||||
|
SessionCookie string
|
||||||
|
Path string
|
||||||
|
Domain string
|
||||||
|
Secure bool
|
||||||
|
HttpOnly bool
|
||||||
|
MaxAge int
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionManager struct {
|
||||||
|
SessionID string
|
||||||
|
Storer session.Store
|
||||||
|
MaxAge int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionManager) Load(ctx context.Context) session.Value {
|
||||||
|
return s.Storer.Load(ctx, s.SessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionManager) Save(ctx context.Context, value session.Value) error {
|
||||||
|
return s.Storer.Save(ctx, s.SessionID, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionManager) SetMaxAge(maxAge int) {
|
||||||
|
s.MaxAge = maxAge
|
||||||
|
}
|
||||||
|
|
||||||
|
func Session(config *SessionConfig, storer session.Store) Middleware {
|
||||||
|
return func(h http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
sessionID := objectid.New().String()
|
||||||
|
cookie, err := r.Cookie(config.SessionCookie)
|
||||||
|
if err == nil {
|
||||||
|
sessionID = cookie.Value
|
||||||
|
}
|
||||||
|
sessionManager := &SessionManager{SessionID: sessionID, Storer: storer, MaxAge: config.MaxAge}
|
||||||
|
|
||||||
|
h.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), sessionKey, &sessionManager)))
|
||||||
|
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: config.SessionCookie,
|
||||||
|
Value: sessionID,
|
||||||
|
Path: config.Path,
|
||||||
|
Domain: config.Domain,
|
||||||
|
Secure: config.Secure,
|
||||||
|
HttpOnly: config.HttpOnly,
|
||||||
|
MaxAge: sessionManager.MaxAge,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SessionFromRequest(r *http.Request) *SessionManager {
|
||||||
|
return r.Context().Value(sessionKey).(*SessionManager)
|
||||||
|
}
|
61
session/bbolt/bbolt.go
Normal file
61
session/bbolt/bbolt.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package bbolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/gob"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"go.etcd.io/bbolt"
|
||||||
|
"go.neonxp.ru/middleware/session"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(db *bbolt.DB, bucketName []byte) session.Store {
|
||||||
|
return &Store{
|
||||||
|
db: db,
|
||||||
|
bucketName: bucketName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
db *bbolt.DB
|
||||||
|
bucketName []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Load(ctx context.Context, sessionID string) session.Value {
|
||||||
|
v := session.Value{}
|
||||||
|
err := s.db.View(func(tx *bbolt.Tx) error {
|
||||||
|
bucket := tx.Bucket(s.bucketName)
|
||||||
|
if bucket == nil {
|
||||||
|
// no bucket -- normal situation
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
vb := bucket.Get([]byte(sessionID))
|
||||||
|
if vb == nil {
|
||||||
|
// no session -- no error
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rdr := bytes.NewBuffer(vb)
|
||||||
|
|
||||||
|
return gob.NewDecoder(rdr).Decode(&v)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
slog.WarnContext(ctx, "failed load session", slog.Any("error", err))
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Save(ctx context.Context, sessionID string, value session.Value) error {
|
||||||
|
return s.db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
bucket, err := tx.CreateBucketIfNotExists(s.bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
wrt := bytes.NewBuffer([]byte{})
|
||||||
|
if err := gob.NewEncoder(wrt).Encode(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bucket.Put([]byte(sessionID), wrt.Bytes())
|
||||||
|
})
|
||||||
|
}
|
7
session/bbolt/go.mod
Normal file
7
session/bbolt/go.mod
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
module gitrepo.ru/neonxp/middleware/session/bbolt
|
||||||
|
|
||||||
|
go 1.22.5
|
||||||
|
|
||||||
|
require go.etcd.io/bbolt v1.3.10
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.4.0 // indirect
|
4
session/bbolt/go.sum
Normal file
4
session/bbolt/go.sum
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
||||||
|
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
||||||
|
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||||
|
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
25
session/memstore.go
Normal file
25
session/memstore.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MemoryStore struct {
|
||||||
|
store sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryStore) Load(ctx context.Context, sessionID string) Value {
|
||||||
|
val, ok := s.store.Load(sessionID)
|
||||||
|
if ok {
|
||||||
|
return val.(Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Value{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryStore) Save(ctx context.Context, sessionID string, value Value) error {
|
||||||
|
s.store.Store(sessionID, value)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
17
session/store.go
Normal file
17
session/store.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrSessionNotFound = errors.New("session not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store interface {
|
||||||
|
Load(ctx context.Context, sessionID string) Value
|
||||||
|
Save(ctx context.Context, sessionID string, value Value) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Value map[string]any
|
Loading…
Reference in a new issue