feat(auth): host-based org resolution + session/host match guard

This commit is contained in:
2026-07-21 16:38:24 +01:00
parent 2038e86b53
commit ff22340561
2 changed files with 72 additions and 0 deletions
+6
View File
@@ -55,6 +55,12 @@ func Middleware() gin.HandlerFunc {
}
c.Set(ctxSessionKey, sess)
if hostOrg, ok := OrgFromHost(c); ok && hostOrg.OrgID != sess.OrgID {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "org host mismatch"})
return
}
c.Next()
}
}
+66
View File
@@ -0,0 +1,66 @@
package auth
import (
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/mrhid6/vantage/server/internal/models"
"github.com/mrhid6/vantage/server/internal/services"
)
type cachedOrg struct {
org *models.Org
at time.Time
}
var (
orgCacheMu sync.Mutex
orgCache = map[string]cachedOrg{}
)
const orgCacheTTL = 60 * time.Second
// hostSlug extracts the leftmost DNS label if the host is a subdomain of the
// app root. Returns "" for the apex or an unknown host shape.
func hostSlug(host string) string {
host = strings.ToLower(host)
if i := strings.IndexByte(host, ':'); i >= 0 {
host = host[:i]
}
// Expect <slug>.vantage.<...>; apex is vantage.<...>
parts := strings.Split(host, ".")
if len(parts) < 3 {
return ""
}
if parts[1] != "vantage" {
return ""
}
if parts[0] == "vantage" || parts[0] == "www" {
return ""
}
return parts[0]
}
func OrgFromHost(c *gin.Context) (*models.Org, bool) {
slug := hostSlug(c.Request.Host)
if slug == "" {
return nil, false
}
orgCacheMu.Lock()
if e, ok := orgCache[slug]; ok && time.Since(e.at) < orgCacheTTL {
orgCacheMu.Unlock()
return e.org, e.org != nil
}
orgCacheMu.Unlock()
org, err := services.GetOrgBySlug(slug)
if err != nil {
org = nil
}
orgCacheMu.Lock()
orgCache[slug] = cachedOrg{org: org, at: time.Now()}
orgCacheMu.Unlock()
return org, org != nil
}