sse support
This commit is contained in:
@@ -111,13 +111,17 @@ func (a *Auth) AuthenticateAndAddIdentity(f response.HandlerFunc, assertions ...
|
||||
return func(c *gin.Context) (response.Response, error) {
|
||||
c, res, err := a.AuthenticateAndAddIdentityToRequest(c, assertions...)
|
||||
if res != nil || err != nil {
|
||||
return res, nil
|
||||
return res, err
|
||||
}
|
||||
|
||||
return f(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Auth) AuthenticateAndAddIdentityGin(assertions ...AuthorizationAssertions) gin.HandlerFunc {
|
||||
return a.AuthenticateAndAddIdentityToRequestGin(assertions...)
|
||||
}
|
||||
|
||||
func (a *Auth) AuthenticateAndAddIdentityToRequest(c *gin.Context, assertions ...AuthorizationAssertions) (*gin.Context, response.Response, error) {
|
||||
r := c.Request
|
||||
ck, err := r.Cookie("access_token")
|
||||
@@ -188,15 +192,129 @@ func (a *Auth) AuthenticateAndAddIdentityToRequest(c *gin.Context, assertions ..
|
||||
return c, nil, nil
|
||||
}
|
||||
|
||||
func (a *Auth) AuthenticateAndAddIdentityToRequestGin(assertions ...AuthorizationAssertions) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
r := c.Request
|
||||
ck, err := r.Cookie("access_token")
|
||||
if err != nil {
|
||||
response.Write(c, response.TemporaryRedirect("/").
|
||||
JSON("no access_token cookie provided"))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
accessToken := ck.Value
|
||||
|
||||
claims, expiration, err := a.auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken)
|
||||
if err != nil {
|
||||
if errors.Is(err, consts.ErrNotFound) {
|
||||
u, err := a.newLoginURL(ctx, a.auth, r.URL.String())
|
||||
if err != nil {
|
||||
response.WriteError(c, response.Errorf("failed to generate login url: %w", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
response.Write(c, response.TemporaryRedirect(u))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteError(c, response.Errorf("failed to authenticate: %w", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// refresh tokens, when the access token is "old enough"
|
||||
|
||||
// id token lifetime is 48 hours, allowing a person to use the app everyday comfortably, with wiggle room, without having to log in.
|
||||
const idTokenLifetime = 48 * time.Hour
|
||||
if refreshFloor := expiration.Add(-(idTokenLifetime / 4)); refreshFloor.Before(now) {
|
||||
accessToken, expiration, err = a.auth.RefreshAccessToken(ctx, accessToken)
|
||||
if err != nil {
|
||||
a.log.Warn("failed to refresh access token", "error", err)
|
||||
response.Write(c, response.TemporaryRedirect("/").
|
||||
Body(io.NopCloser(bytes.NewBuffer([]byte(fmt.Sprintf("failed to refresh access token: %v", err))))).
|
||||
Cookie(cookies.Expired("access_token")))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 'redirect' to same url, to set the new access_token cookie
|
||||
response.Write(c, response.TemporaryRedirect(r.URL.String()).
|
||||
Cookie(cookies.AccessToken(accessToken, expiration)))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// add identity info to request context
|
||||
|
||||
user, acct, err := a.accts.GetUserAndAccountByAccessToken(ctx, accessToken)
|
||||
if err != nil {
|
||||
response.WriteError(c, response.Errorf("failed to authorize: %w", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
for _, as := range assertions {
|
||||
res, err := as(c)
|
||||
if err != nil {
|
||||
response.WriteError(c, err)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if res != nil {
|
||||
response.Write(c, res)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
id := Identity{
|
||||
AccessToken: accessToken,
|
||||
Claims: claims,
|
||||
User: user,
|
||||
Account: acct,
|
||||
}
|
||||
|
||||
c.Set(identityKeyString, id)
|
||||
c.Request = r.WithContext(SetIdentity(ctx, id))
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
type identityKey struct{}
|
||||
|
||||
// stores identity in request context
|
||||
const identityKeyString = "identity"
|
||||
|
||||
// stores identity in context
|
||||
func SetIdentity(ctx context.Context, id Identity) context.Context {
|
||||
return context.WithValue(ctx, identityKey{}, id)
|
||||
}
|
||||
|
||||
// get identity from request context
|
||||
// get identity from context
|
||||
func GetIdentity(ctx context.Context) Identity {
|
||||
id, _ := ctx.Value(identityKey{}).(Identity)
|
||||
id, ok := ctx.Value(identityKey{}).(Identity)
|
||||
if ok {
|
||||
return id
|
||||
}
|
||||
|
||||
c, ok := ctx.(*gin.Context)
|
||||
if !ok {
|
||||
return Identity{}
|
||||
}
|
||||
|
||||
v, ok := c.Get(identityKeyString)
|
||||
if !ok {
|
||||
return Identity{}
|
||||
}
|
||||
|
||||
id, _ = v.(Identity)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"ruben/inventory2/internal/server/response"
|
||||
)
|
||||
|
||||
func Chain(ms ...response.Middleware) response.Middleware {
|
||||
return func(fn response.HandlerFunc) response.HandlerFunc {
|
||||
res := fn
|
||||
for _, m := range ms {
|
||||
prev := res
|
||||
res = m(prev)
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path"
|
||||
"ruben/inventory2/internal/logging"
|
||||
"ruben/inventory2/internal/server/sse"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type UpdateNotificationPublisher struct {
|
||||
log *logging.Logger
|
||||
sse *sse.Queue
|
||||
trimBasePath string
|
||||
basePathPattern string
|
||||
}
|
||||
|
||||
func NewUpdateNotificationPublisher(
|
||||
log *logging.Logger,
|
||||
sse *sse.Queue,
|
||||
) *UpdateNotificationPublisher {
|
||||
return &UpdateNotificationPublisher{
|
||||
log: log,
|
||||
sse: sse,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *UpdateNotificationPublisher) Trim(pathPattern string) *UpdateNotificationPublisher {
|
||||
p2 := *p
|
||||
p2.trimBasePath = path.Join(p2.trimBasePath, pathPattern)
|
||||
return &p2
|
||||
}
|
||||
|
||||
func (p *UpdateNotificationPublisher) Group(pathPattern string) *UpdateNotificationPublisher {
|
||||
p2 := *p
|
||||
p2.basePathPattern = path.Join(p2.basePathPattern, pathPattern)
|
||||
return &p2
|
||||
}
|
||||
|
||||
func (p *UpdateNotificationPublisher) Publish(pathPattern string) gin.HandlerFunc {
|
||||
trimBasePathSegs := getPathSegments(p.trimBasePath)
|
||||
trimmedBasePathPattern := path.Join(p.basePathPattern, pathPattern)
|
||||
trimmedBasePathPatternSegs := getPathSegments(trimmedBasePathPattern)
|
||||
fullBasePathPatternSegs := append(trimBasePathSegs, trimmedBasePathPatternSegs...)
|
||||
|
||||
return func(c *gin.Context) {
|
||||
reqPathSegs := getPathSegments(c.Request.URL.Path)
|
||||
|
||||
c.Next()
|
||||
if len(c.Errors) > 0 {
|
||||
return
|
||||
}
|
||||
if len(reqPathSegs) < len(fullBasePathPatternSegs) {
|
||||
p.log.Errorf("req path is less that the full path: %v, %v", c.Request.URL.Path, path.Join(p.trimBasePath, p.basePathPattern, pathPattern))
|
||||
return
|
||||
}
|
||||
|
||||
// if the path is a subpath, then emit events along the subpath.
|
||||
|
||||
for i, s := range trimmedBasePathPatternSegs {
|
||||
if isWildcard := s[0] == ':'; isWildcard {
|
||||
continue
|
||||
}
|
||||
|
||||
rs := reqPathSegs[i]
|
||||
if isSubpath := s != rs; isSubpath {
|
||||
continue
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
topEventParts := reqPathSegs[len(trimBasePathSegs):len(fullBasePathPatternSegs)]
|
||||
topEvent := strings.Join(topEventParts, "_")
|
||||
|
||||
events := make([]string, len(reqPathSegs)-len(fullBasePathPatternSegs)+1)
|
||||
events[0] = topEvent
|
||||
parentEvent := topEvent
|
||||
for i, s := range reqPathSegs[len(fullBasePathPatternSegs):] {
|
||||
e := parentEvent + "_" + s
|
||||
events[i+1] = e
|
||||
parentEvent = e
|
||||
}
|
||||
|
||||
for _, e := range events {
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
if err := p.sse.Send(ctx, e, nil); err != nil {
|
||||
p.log.Errorf("failed to send sse event to listener: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getPathSegments(p string) []string {
|
||||
p = path.Clean(p)
|
||||
if p == "" || p == "." || p == "/" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return strings.Split(strings.Trim(p, "/"), "/")
|
||||
}
|
||||
Reference in New Issue
Block a user