refactor(feeds): extract atom feed builder + basic-auth validator for reuse

Splits the transport-agnostic cores out of the v1 echo handlers so the
v2 Huma endpoints can share them:

- AuthenticateFeedToken(s, username, password) holds the token
  validation (prefix/length guard, owner match, feeds scope, bot
  rejection); BasicAuth now creates the session and delegates to it.
- BuildNotificationsAtomFeed(s, u) renders the Atom XML;
  NotificationsAtomFeed reads the context user and delegates to it.
- AtomContentType is shared so both transports set the same header.

The v1 handlers keep identical observable behavior.
This commit is contained in:
kolaente 2026-06-17 21:37:54 +02:00
parent 5c0e266042
commit be8a092fe3
2 changed files with 51 additions and 26 deletions

View File

@ -23,9 +23,9 @@ import (
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user"
"xorm.io/xorm"
"github.com/labstack/echo/v5"
"xorm.io/xorm"
)
func checkAPIToken(s *xorm.Session, username, token string) (*user.User, error) {
@ -50,35 +50,48 @@ func checkAPIToken(s *xorm.Session, username, token string) (*user.User, error)
return u, nil
}
// BasicAuth authenticates feed requests. Only API tokens are accepted —
// password and LDAP credentials are rejected outright because feed URLs are
// commonly exported, shared, or cached by feed readers.
func BasicAuth(c *echo.Context, username, password string) (bool, error) {
// AuthenticateFeedToken validates feed credentials against an existing session.
// Only API tokens are accepted — password and LDAP credentials are rejected
// outright because feed URLs are commonly exported, shared, or cached by feed
// readers. It returns the authenticated user, or nil for any rejection so
// callers can treat "invalid" and "unknown" identically.
func AuthenticateFeedToken(s *xorm.Session, username, password string) (*user.User, error) {
if !strings.HasPrefix(password, models.APITokenPrefix) {
return false, nil
return nil, nil
}
// GetTokenFromTokenString slices password[len-8:] without a length check,
// so a stray "tk_" or other short prefix-only string would panic before
// the credentials could be rejected. Real tokens are far longer than
// prefix+8, so anything shorter is invalid by construction.
if len(password) < len(models.APITokenPrefix)+8 {
return false, nil
return nil, nil
}
s := db.NewSession()
defer s.Close()
u, err := checkAPIToken(s, username, password)
if err != nil {
log.Errorf("Error during API token auth for feeds: %v", err)
return false, nil
return nil, nil
}
if u == nil {
return false, nil
return nil, nil
}
if u.IsBot() {
log.Warningf("Feed auth rejected for bot user %d", u.ID)
return false, nil
return nil, nil
}
return u, nil
}
// BasicAuth authenticates feed requests for echo's BasicAuth middleware. The
// validation logic is shared with the v2 handler via AuthenticateFeedToken.
func BasicAuth(c *echo.Context, username, password string) (bool, error) {
s := db.NewSession()
defer s.Close()
u, err := AuthenticateFeedToken(s, username, password)
if err != nil || u == nil {
return false, err
}
c.Set("userBasicAuth", u)

View File

@ -30,24 +30,22 @@ import (
"github.com/gorilla/feeds"
"github.com/labstack/echo/v5"
"xorm.io/xorm"
)
const feedItemLimit = 50
// NotificationsAtomFeed serves the authenticated user's notifications as an
// Atom feed. Notifications are not marked as read by being fetched here.
func NotificationsAtomFeed(c *echo.Context) error {
u, ok := c.Get("userBasicAuth").(*user.User)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized))
}
s := db.NewSession()
defer s.Close()
// AtomContentType is the content type of the notifications Atom feed. Shared so
// the v1 echo handler and the v2 Huma op set the same header.
const AtomContentType = "application/atom+xml; charset=utf-8"
// BuildNotificationsAtomFeed renders the user's latest notifications as Atom XML
// against an existing session. Notifications are not marked as read by being
// fetched here. Shared by the v1 echo handler and the v2 Huma op.
func BuildNotificationsAtomFeed(s *xorm.Session, u *user.User) (string, error) {
rows, _, _, err := notifications.GetNotificationsForUser(s, u.ID, feedItemLimit, 0)
if err != nil {
return err
return "", err
}
publicURL := config.ServicePublicURL.GetString()
@ -85,11 +83,25 @@ func NotificationsAtomFeed(c *echo.Context) error {
})
}
atom, err := feed.ToAtom()
return feed.ToAtom()
}
// NotificationsAtomFeed serves the authenticated user's notifications as an
// Atom feed. Notifications are not marked as read by being fetched here.
func NotificationsAtomFeed(c *echo.Context) error {
u, ok := c.Get("userBasicAuth").(*user.User)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized))
}
s := db.NewSession()
defer s.Close()
atom, err := BuildNotificationsAtomFeed(s, u)
if err != nil {
return err
}
c.Response().Header().Set(echo.HeaderContentType, "application/atom+xml; charset=utf-8")
c.Response().Header().Set(echo.HeaderContentType, AtomContentType)
return c.String(http.StatusOK, atom)
}