feat(plugins): allow plugins to register routes

This commit is contained in:
kolaente
2025-07-24 15:55:15 +02:00
parent ec6ee7632f
commit e5c860afec
4 changed files with 108 additions and 2 deletions

View File

@@ -17,12 +17,17 @@
package main
import (
"net/http"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/plugins"
"code.vikunja.io/api/pkg/user"
"github.com/ThreeDotsLabs/watermill/message"
"github.com/labstack/echo/v4"
)
type ExamplePlugin struct{}
@@ -38,6 +43,55 @@ func (p *ExamplePlugin) Init() error {
}
func (p *ExamplePlugin) Shutdown() error { return nil }
// RegisterAuthenticatedRoutes implements the AuthenticatedRouterPlugin interface
func (p *ExamplePlugin) RegisterAuthenticatedRoutes(g *echo.Group) {
g.GET("/user-info", handleUserInfo)
log.Infof("example plugin authenticated routes registered")
}
// RegisterUnauthenticatedRoutes implements the UnauthenticatedRouterPlugin interface
func (p *ExamplePlugin) RegisterUnauthenticatedRoutes(g *echo.Group) {
g.GET("/status", handleStatus)
log.Infof("example plugin unauthenticated routes registered")
}
// Authenticated route handlers
func handleUserInfo(c echo.Context) error {
s := db.NewSession()
defer s.Close()
// Get the authenticated user from context
u, err := user.GetCurrentUserFromDB(s, c)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "User not found")
}
p := &ExamplePlugin{}
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Hello from example plugin!",
"user": u,
"plugin": p.Name(),
"version": p.Version(),
})
}
// Unauthenticated route handlers
func handleStatus(c echo.Context) error {
p := &ExamplePlugin{}
return c.JSON(http.StatusOK, map[string]interface{}{
"status": "ok",
"plugin": p.Name(),
"version": p.Version(),
"message": "Example plugin is running",
})
}
func NewPlugin() plugins.Plugin { return &ExamplePlugin{} }
type TestListener struct{}

View File

@@ -17,6 +17,7 @@
package plugins
import (
"github.com/labstack/echo/v4"
"src.techknowlogick.com/xormigrate"
)
@@ -33,3 +34,15 @@ type MigrationPlugin interface {
Plugin
Migrations() []*xormigrate.Migration
}
// AuthenticatedRouterPlugin lets a plugin register authenticated web handlers and routes.
type AuthenticatedRouterPlugin interface {
Plugin
RegisterAuthenticatedRoutes(g *echo.Group)
}
// UnauthenticatedRouterPlugin lets a plugin register unauthenticated web handlers and routes.
type UnauthenticatedRouterPlugin interface {
Plugin
RegisterUnauthenticatedRoutes(g *echo.Group)
}

View File

@@ -25,12 +25,16 @@ import (
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/migration"
"github.com/labstack/echo/v4"
)
// Manager handles loading and managing plugins.
type Manager struct {
plugins []Plugin
migrationPlugs []MigrationPlugin
plugins []Plugin
migrationPlugs []MigrationPlugin
authenticatedRouterPlugs []AuthenticatedRouterPlugin
unauthenticatedRouterPlugs []UnauthenticatedRouterPlugin
}
var manager = &Manager{}
@@ -70,6 +74,21 @@ func Shutdown() {
}
}
// RegisterPluginRoutes registers routes from all router plugins.
func RegisterPluginRoutes(authenticated *echo.Group, unauthenticated *echo.Group) {
// Register authenticated routes
for _, p := range manager.authenticatedRouterPlugs {
p.RegisterAuthenticatedRoutes(authenticated)
log.Debugf("Registered authenticated routes for plugin %s", p.Name())
}
// Register unauthenticated routes
for _, p := range manager.unauthenticatedRouterPlugs {
p.RegisterUnauthenticatedRoutes(unauthenticated)
log.Debugf("Registered unauthenticated routes for plugin %s", p.Name())
}
}
func (m *Manager) loadPlugins(paths []string) error {
for _, p := range paths {
entries, err := os.ReadDir(p)
@@ -113,6 +132,14 @@ func (m *Manager) loadPlugin(path string) error {
migration.AddPluginMigrations(mp.Migrations())
}
if arp, ok := p.(AuthenticatedRouterPlugin); ok {
m.authenticatedRouterPlugs = append(m.authenticatedRouterPlugs, arp)
}
if urp, ok := p.(UnauthenticatedRouterPlugin); ok {
m.unauthenticatedRouterPlugs = append(m.unauthenticatedRouterPlugs, urp)
}
log.Infof("Loaded plugin %s", p.Name())
return nil

View File

@@ -73,6 +73,7 @@ import (
"code.vikunja.io/api/pkg/modules/migration/todoist"
"code.vikunja.io/api/pkg/modules/migration/trello"
vikunja_file "code.vikunja.io/api/pkg/modules/migration/vikunja-file"
"code.vikunja.io/api/pkg/plugins"
apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
"code.vikunja.io/api/pkg/routes/caldav"
"code.vikunja.io/api/pkg/version"
@@ -644,6 +645,17 @@ func registerAPIRoutes(a *echo.Group) {
},
}
a.POST("/projects/:project/views/:view/buckets/:bucket/tasks", taskBucketProvider.UpdateWeb)
// Plugin routes
if config.PluginsEnabled.GetBool() {
// Authenticated plugin routes
authenticatedPluginGroup := a.Group("/plugins")
// Unauthenticated plugin routes (with basic IP rate limiting)
unauthenticatedPluginGroup := n.Group("/plugins")
plugins.RegisterPluginRoutes(authenticatedPluginGroup, unauthenticatedPluginGroup)
}
}
func registerMigrations(m *echo.Group) {