package auth import ( "net/http" "strings" "tyto/internal/database" "github.com/gin-gonic/gin" ) const ( // ContextKeyUser is the key for the authenticated user in gin.Context. ContextKeyUser = "auth_user" // ContextKeySession is the key for the session in gin.Context. ContextKeySession = "auth_session" ) // Middleware provides authentication middleware for Gin. type Middleware struct { auth *Service } // NewMiddleware creates a new authentication middleware. func NewMiddleware(auth *Service) *Middleware { return &Middleware{auth: auth} } // Required returns middleware that requires authentication. // Requests without valid authentication are rejected with 401. func (m *Middleware) Required() gin.HandlerFunc { return func(c *gin.Context) { session, user, err := m.authenticate(c) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "error": "authentication required", }) return } c.Set(ContextKeyUser, user) c.Set(ContextKeySession, session) c.Next() } } // Optional returns middleware that attempts authentication but doesn't require it. // Unauthenticated requests are allowed to proceed. func (m *Middleware) Optional() gin.HandlerFunc { return func(c *gin.Context) { session, user, err := m.authenticate(c) if err == nil { c.Set(ContextKeyUser, user) c.Set(ContextKeySession, session) } c.Next() } } // authenticate extracts and validates the session token. func (m *Middleware) authenticate(c *gin.Context) (*database.Session, *database.User, error) { token := m.extractToken(c) if token == "" { return nil, nil, ErrInvalidToken } return m.auth.ValidateSession(c.Request.Context(), token) } // extractToken gets the session token from the request. // Checks Authorization header first, then cookies. func (m *Middleware) extractToken(c *gin.Context) string { // Check Authorization header: "Bearer " auth := c.GetHeader("Authorization") if strings.HasPrefix(auth, "Bearer ") { return strings.TrimPrefix(auth, "Bearer ") } // Check cookie if cookie, err := c.Cookie(m.auth.config.SessionCookieName); err == nil { return cookie } return "" } // GetUser returns the authenticated user from the context. // Returns nil if not authenticated. func GetUser(c *gin.Context) *database.User { user, exists := c.Get(ContextKeyUser) if !exists { return nil } return user.(*database.User) } // GetSession returns the session from the context. // Returns nil if not authenticated. func GetSession(c *gin.Context) *database.Session { session, exists := c.Get(ContextKeySession) if !exists { return nil } return session.(*database.Session) } // MustGetUser returns the authenticated user from the context. // Panics if not authenticated (use after Required middleware). func MustGetUser(c *gin.Context) *database.User { user := GetUser(c) if user == nil { panic("auth: MustGetUser called without authentication") } return user } // SetSessionCookie sets the session cookie on the response. func (m *Middleware) SetSessionCookie(c *gin.Context, session *database.Session) { maxAge := int(m.auth.config.SessionDuration.Seconds()) c.SetSameSite(http.SameSiteLaxMode) c.SetCookie( m.auth.config.SessionCookieName, session.Token, maxAge, m.auth.config.SessionCookiePath, "", // domain (empty = current domain) m.auth.config.SessionCookieHTTPS, true, // httpOnly ) } // ClearSessionCookie removes the session cookie. func (m *Middleware) ClearSessionCookie(c *gin.Context) { c.SetSameSite(http.SameSiteLaxMode) c.SetCookie( m.auth.config.SessionCookieName, "", -1, // MaxAge -1 = delete m.auth.config.SessionCookiePath, "", m.auth.config.SessionCookieHTTPS, true, ) }